Lambdas, avant multiple avec moulage

besoin d'aide pour penser à lambdas de la part de mes collègues luminaires Stacoverflow.

cas Standard de sélection à travers une liste d'une liste d'une liste pour recueillir quelques enfants profondément dans un graphique. Quels moyens impressionnants pourraient Lambdas aide avec ce boilerplate?

public List<ContextInfo> list() {
    final List<ContextInfo> list = new ArrayList<ContextInfo>();
    final StandardServer server = getServer();

    for (final Service service : server.findServices()) {
        if (service.getContainer() instanceof Engine) {
            final Engine engine = (Engine) service.getContainer();
            for (final Container possibleHost : engine.findChildren()) {
                if (possibleHost instanceof Host) {
                    final Host host = (Host) possibleHost;
                    for (final Container possibleContext : host.findChildren()) {
                        if (possibleContext instanceof Context) {
                            final Context context = (Context) possibleContext;
                            // copy to another object -- not the important part
                            final ContextInfo info = new ContextInfo(context.getPath());
                            info.setThisPart(context.getThisPart());
                            info.setNotImportant(context.getNotImportant());
                            list.add(info);
                        }
                    }
                }
            }
        }
    }
    return list;
}

Remarque: la liste elle-même est d'aller vers le client JSON, alors ne vous concentrez pas sur ce qui est retourné. Il doit y avoir quelques façons de couper les boucles.

intéressé de voir ce que mes collègues experts créer. Plusieurs approches sont encouragées.

EDIT

findServices et les deux findChildren méthodes retourner les tableaux

EDIT-BONUS CHALLENGE

la "partie non importante" s'est avérée être importante. J'ai réellement besoin de copier une valeur uniquement disponible dans le host instance. Cela semble ruiner tous les beaux exemples. Comment pourrait-on réaliser l'état de l'avant?

final ContextInfo info = new ContextInfo(context.getPath());
info.setHostname(host.getName()); // The Bonus Challenge
27
demandé sur David Blevins 2014-08-22 07:52:57

4 réponses

il est assez profondément imbriqué mais il ne semble pas exceptionnellement difficile.

la première observation est que si une boucle for se traduit par un flux, les boucles For-nestées peuvent être "aplaties" en un seul flux en utilisant flatMap. Cette opération prend un seul élément et renvoie un nombre arbitraire d'éléments dans un flux. J'ai regardé et constaté que StandardServer.findServices() retourne un tableau de Service alors nous transformons ceci en un flux en utilisant Arrays.stream(). (Je fais des hypothèses analogues pour Engine.findChildren() et Host.findChildren().

Ensuite, la logique à l'intérieur de chaque boucle n' instanceof vérifier et d'un casting. Cela peut être modélisé en utilisant des flux comme un filter opération à réaliser l' instanceof suivi de map opération qui jette et renvoie simplement la même référence. Il s'agit en fait d'un no-op mais il permet au système de frappe statique de convertir un Stream<Container>Stream<Host> par exemple.

appliquant ces transformations aux boucles imbriquées, on obtient ce qui suit:

public List<ContextInfo> list() {
    final List<ContextInfo> list = new ArrayList<ContextInfo>();
    final StandardServer server = getServer();

    Arrays.stream(server.findServices())
        .filter(service -> service.getContainer() instanceof Engine)
        .map(service -> (Engine)service.getContainer())
        .flatMap(engine -> Arrays.stream(engine.findChildren()))
        .filter(possibleHost -> possibleHost instanceof Host)
        .map(possibleHost -> (Host)possibleHost)
        .flatMap(host -> Arrays.stream(host.findChildren()))
        .filter(possibleContext -> possibleContext instanceof Context)
        .map(possibleContext -> (Context)possibleContext)
        .forEach(context -> {
            // copy to another object -- not the important part
            final ContextInfo info = new ContextInfo(context.getPath());
            info.setThisPart(context.getThisPart());
            info.setNotImportant(context.getNotImportant());
            list.add(info);
        });
    return list;
}

Mais attendez, il y a plus.

final forEach opération est un peu plus compliqué map opération qui convertit un Context dans un ContextInfo. En outre, ils sont simplement recueillis dans un List donc nous pouvons utiliser des collecteurs pour faire cela au lieu de créer et vider la liste à l'avance et puis la remplir. L'application de ces modifications donne les résultats suivants:

public List<ContextInfo> list() {
    final StandardServer server = getServer();

    return Arrays.stream(server.findServices())
        .filter(service -> service.getContainer() instanceof Engine)
        .map(service -> (Engine)service.getContainer())
        .flatMap(engine -> Arrays.stream(engine.findChildren()))
        .filter(possibleHost -> possibleHost instanceof Host)
        .map(possibleHost -> (Host)possibleHost)
        .flatMap(host -> Arrays.stream(host.findChildren()))
        .filter(possibleContext -> possibleContext instanceof Context)
        .map(possibleContext -> (Context)possibleContext)
        .map(context -> {
            // copy to another object -- not the important part
            final ContextInfo info = new ContextInfo(context.getPath());
            info.setThisPart(context.getThisPart());
            info.setNotImportant(context.getNotImportant());
            return info;
        })
        .collect(Collectors.toList());
}

j'essaie habituellement d'éviter les lambdas multi-lignes (comme dans la finale map opération) donc j'aurais refactoriser le code dans une petite aide de la méthode qui prend un Context et renvoie un ContextInfo. Cela ne raccourcit pas le code du tout, mais je pense que cela le rend plus clair.

UPDATE

mais attendez, il y a encore plus.

extrayons l'appel à service.getContainer() dans son propre pipeline de l'élément:

    return Arrays.stream(server.findServices())
        .map(service -> service.getContainer())
        .filter(container -> container instanceof Engine)
        .map(container -> (Engine)container)
        .flatMap(engine -> Arrays.stream(engine.findChildren()))
        // ...

ceci expose la répétition du filtrage sur instanceof suivi d'un mapping avec un plâtre. Faites cela trois fois au total. Il il semble probable que d'autres codes vont avoir besoin de faire des choses similaires, donc il serait bien d'extraire ce peu de logique dans une méthode d'aide. Le problème est que filter peut changer le nombre d'éléments dans le flux (en laissant tomber ceux qui ne correspondent pas) mais il ne peut pas changer leurs types. Et map pouvez modifier les types d'éléments, mais il ne peut pas changer de numéro. Quelque chose peut changer à la fois le nombre et les types? Oui, c'est notre vieil ami flatMap encore une fois! Donc notre méthode d'aide doit prendre un élément et le retour d'un flux d'éléments d'un type différent. Ce flux de retour contiendra un seul élément moulé (s'il correspond) ou il sera vide (s'il ne correspond pas). La fonction helper ressemblerait à ceci:

<T,U> Stream<U> toType(T t, Class<U> clazz) {
    if (clazz.isInstance(t)) {
        return Stream.of(clazz.cast(t));
    } else {
        return Stream.empty();
    }
}

(C'est vaguement basé sur C#OfType construction mentionnée dans certains des commentaires.)

tant que nous y sommes, extrayons une méthode pour créer un ContextInfo:

ContextInfo makeContextInfo(Context context) {
    // copy to another object -- not the important part
    final ContextInfo info = new ContextInfo(context.getPath());
    info.setThisPart(context.getThisPart());
    info.setNotImportant(context.getNotImportant());
    return info;
}

Après ces extractions, le pipeline ressemble ceci:

    return Arrays.stream(server.findServices())
        .map(service -> service.getContainer())
        .flatMap(container -> toType(container, Engine.class))
        .flatMap(engine -> Arrays.stream(engine.findChildren()))
        .flatMap(possibleHost -> toType(possibleHost, Host.class))
        .flatMap(host -> Arrays.stream(host.findChildren()))
        .flatMap(possibleContext -> toType(possibleContext, Context.class))
        .map(this::makeContextInfo)
        .collect(Collectors.toList());

plus agréable, je pense, et nous avons enlevé la redoutable déclaration à plusieurs lignes lambda.

MISE À JOUR: BONUS CHALLENGE

encore une Fois, flatMap est votre ami. Prenez la queue du ruisseau et migrez-la dans la dernière flatMap avant la queue. De cette façon, le host la variable est toujours en scope, et vous pouvez la passer à un makeContextInfo méthode helper qui a été modifiée pour prendre host.

    return Arrays.stream(server.findServices())
        .map(service -> service.getContainer())
        .flatMap(container -> toType(container, Engine.class))
        .flatMap(engine -> Arrays.stream(engine.findChildren()))
        .flatMap(possibleHost -> toType(possibleHost, Host.class))
        .flatMap(host -> Arrays.stream(host.findChildren())
                               .flatMap(possibleContext -> toType(possibleContext, Context.class))
                               .map(ctx -> makeContextInfo(ctx, host)))
        .collect(Collectors.toList());
35
répondu Stuart Marks 2014-08-23 03:13:29

ce serait ma version de votre code en utilisant JDK 8 streams, method references et lambda expressions:

server.findServices()
    .stream()
    .map(Service::getContainer)
    .filter(Engine.class::isInstance)
    .map(Engine.class::cast)
    .flatMap(engine -> Arrays.stream(engine.findChildren()))
    .filter(Host.class::isInstance)
    .map(Host.class::cast)
    .flatMap(host -> Arrays.stream(host.findChildren()))
    .filter(Context.class::isInstance)
    .map(Context.class::cast)
    .map(context -> {
        ContextInfo info = new ContextInfo(context.getPath());
        info.setThisPart(context.getThisPart());
        info.setNotImportant(context.getNotImportant());
        return info;
    })
    .collect(Collectors.toList());

dans cette approche je remplace vos if-statements pour filtrer les prédicats. Prendre en compte qu'un instanceof vérification peut être remplacé par un Predicate<T>

Predicate<Object> isEngine = someObject -> someObject instanceof Engine;

qui peut aussi être exprimé par

Predicate<Object> isEngine = Engine.class::isInstance

de Même, vos moulages peuvent être remplacés par Function<T,R>.

Function<Object,Engine> castToEngine = someObject -> (Engine) someObject;

ce qui est à peu près le même

Function<Object,Engine> castToEngine = Engine.class::cast;

et ajouter des éléments manuellement à une liste dans la boucle for peut être remplacé par un collecteur. Dans le code de production, la lambda qui transforme un Context dans un ContextInfo peut (et doit) être extraite dans une méthode distincte et utilisée comme référence de méthode.

26
répondu Edwin Dalorzo 2014-08-22 07:08:32

Solution de bonus challenge

inspiré par la réponse de @EdwinDalorzo.

public List<ContextInfo> list() {
    final List<ContextInfo> list = new ArrayList<>();
    final StandardServer server = getServer();

    return server.findServices()
            .stream()
            .map(Service::getContainer)
            .filter(Engine.class::isInstance)
            .map(Engine.class::cast)
            .flatMap(engine -> Arrays.stream(engine.findChildren()))
            .filter(Host.class::isInstance)
            .map(Host.class::cast)
            .flatMap(host -> mapContainers(
                Arrays.stream(host.findChildren()), host.getName())
            )
            .collect(Collectors.toList());
}

private static Stream<ContextInfo> mapContainers(Stream<Container> containers,
    String hostname) {
    return containers
            .filter(Context.class::isInstance)
            .map(Context.class::cast)
            .map(context -> {
                ContextInfo info = new ContextInfo(context.getPath());
                info.setThisPart(context.getThisPart());
                info.setNotImportant(context.getNotImportant());
                info.setHostname(hostname); // The Bonus Challenge
                return info;
            });
}
2
répondu user11153 2014-08-22 17:43:50

première tentative au-delà de la laideur. Il me faudra des années avant de trouver ça lisible. Doit y avoir une meilleure façon.

Notez le findChildren les méthodes renvoient des tableaux qui, bien sûr, fonctionnent avec for (N n: array) la syntaxe, mais pas avec la nouvelle Iterable.forEach méthode. Avait les envelopper avec Arrays.asList

public List<ContextInfo> list() {
    final List<ContextInfo> list = new ArrayList<ContextInfo>();
    final StandardServer server = getServer();

    asList(server.findServices()).forEach(service -> {

        if (!(service.getContainer() instanceof Engine)) return;

        final Engine engine = (Engine) service.getContainer();

        instanceOf(Host.class, asList(engine.findChildren())).forEach(host -> {

            instanceOf(Context.class, asList(host.findChildren())).forEach(context -> {

                // copy to another object -- not the important part
                final ContextInfo info = new ContextInfo(context.getPath());
                info.setThisPart(context.getThisPart());
                info.setNotImportant(context.getNotImportant());
                list.add(info);
            });
        });
    });

    return list;
}

les méthodes d'utilité

public static <T> Iterable<T> instanceOf(final Class<T> type, final Collection collection) {
    final Iterator iterator = collection.iterator();
    return () -> new SlambdaIterator<>(() -> {
        while (iterator.hasNext()) {
            final Object object = iterator.next();
            if (object != null && type.isAssignableFrom(object.getClass())) {
                return (T) object;
            }
        }
        throw new NoSuchElementException();
    });
}

et enfin une implémentation Lambda-powerable de Iterable

public static class SlambdaIterator<T> implements Iterator<T> {
    // Ya put your Lambdas in there
    public static interface Advancer<T> {
        T advance() throws NoSuchElementException;
    }
    private final Advancer<T> advancer;
    private T next;

    protected SlambdaIterator(final Advancer<T> advancer) {
        this.advancer = advancer;
    }

    @Override
    public boolean hasNext() {
        if (next != null) return true;

        try {
            next = advancer.advance();

            return next != null;
        } catch (final NoSuchElementException e) {
            return false;
        }
    }

    @Override
    public T next() {
        if (!hasNext()) throw new NoSuchElementException();

        final T v = next;
        next = null;
        return v;
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }
}

beaucoup de plomberie et sans doute 5x le code octet. Doit être un meilleure façon de faire.

1
répondu David Blevins 2014-08-22 05:11:52