web-dev-qa-db-fra.com

Lambdas, multiples pour chacun avec casting

Besoin d'aide pour penser en lambdas à mes collègues luminaires StackOverflow.

Cas standard de sélection d'une liste d'une liste pour collecter des enfants au fond d'un graphique. De quelles manières impressionnantes Lambdas pourrait aider avec ce passe-partout?

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;
}

Notez que la liste elle-même va au client sous la forme JSON, donc ne vous concentrez pas sur ce qui est retourné. Doit être quelques façons soignées de couper les boucles.

Intéressé de voir ce que mes collègues experts créent. Plusieurs approches encouragées.

MODIFIER

Les méthodes findServices et les deux méthodes findChildren renvoient des tableaux

EDIT - BONUS CHALLENGE

La "partie non importante" s'est avérée importante. J'ai en fait besoin de copier une valeur disponible uniquement dans l'instance Host. Cela semble ruiner tous les beaux exemples. Comment ferait-on avancer l'État?

final ContextInfo info = new ContextInfo(context.getPath());
info.setHostname(Host.getName()); // The Bonus Challenge
27
David Blevins

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 en flux, les boucles for imbriqué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 recherché et trouvé que StandardServer.findServices() renvoie un tableau de Service donc nous transformons cela en un flux en utilisant Arrays.stream(). (Je fais des hypothèses similaires pour Engine.findChildren() et Host.findChildren().

Ensuite, la logique de chaque boucle effectue une vérification instanceof et une conversion. Cela peut être modélisé en utilisant des flux en tant qu'opération filter pour effectuer instanceof suivie d'une opération map qui transforme simplement et renvoie la même référence. Il s'agit en fait d'un no-op mais cela permet au système de typage statique de convertir un Stream<Container> En un Stream<Host> Par exemple.

En appliquant ces transformations aux boucles imbriquées, nous obtenons 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.

L'opération finale forEach est une opération map légèrement plus compliquée qui convertit un Context en ContextInfo. En outre, ceux-ci sont simplement collectés dans un List afin que nous puissions utiliser des collecteurs pour le faire au lieu de créer et de vider la liste à l'avance, puis de la remplir. L'application de ces refactorisations 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 généralement d'éviter les lambdas sur plusieurs lignes (comme dans l'opération finale de map), je le refactoriserais donc en une petite méthode d'aide qui prend un Context et renvoie un ContextInfo. Cela ne raccourcit pas du tout le code, mais je pense que cela le rend plus clair.

MISE À JOUR

Mais attendez, il y a encore plus.

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

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

Cela expose la répétition du filtrage sur instanceof suivi d'un mappage avec un cast. Cela se fait trois fois au total. Il semble probable que d'autres codes vont devoir faire des choses similaires, il serait donc bien d'extraire ce peu de logique dans une méthode d'assistance. Le problème est que filter peut changer le nombre d'éléments dans le flux (en supprimant ceux qui ne correspondent pas) mais il ne peut pas changer leurs types. Et map peut changer les types d'éléments, mais pas leur nombre. Quelque chose peut-il changer à la fois le nombre et les types? Oui, c'est encore notre vieil ami flatMap! Notre méthode d'assistance doit donc prendre un élément et renvoyer un flux d'éléments d'un type différent. Ce flux de retour contiendra un seul élément casté (s'il correspond) ou il sera vide (s'il ne correspond pas). La fonction d'assistance 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();
    }
}

(Ceci est vaguement basé sur la construction OfType de C # mentionnée dans certains commentaires.)

Pendant 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 supprimé la redoutée déclaration lambda sur plusieurs lignes.

MISE À JOUR: BONUS CHALLENGE

Encore une fois, flatMap est votre ami. Prenez la queue du ruisseau et migrez-la dans le dernier flatMap avant la queue. De cette façon, la variable Host est toujours dans la portée, et vous pouvez la passer à une méthode d'assistance makeContextInfo qui a été modifiée pour prendre également 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());
36
Stuart Marks

Ce serait ma version de votre code utilisant les flux JDK 8, les références de méthode et les expressions lambda:

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 instructions if pour les prédicats de filtre. Tenez compte du fait qu'un instanceof chèque peut être remplacé par un Predicate<T>

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

qui peut également être exprimé comme

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

De même, vos conversions peuvent être remplacées par Function<T,R>.

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

Ce qui est à peu près la même chose que

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

Et l'ajout manuel d'éléments à une liste dans la boucle for peut être remplacé par un collecteur. Dans le code de production, le lambda qui transforme un Context en ContextInfo peut (et doit) être extrait dans une méthode distincte et utilisé comme référence de méthode.

26
Edwin Dalorzo

Solution au défi bonus

Inspiré par la réponse @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
user11153

Première tentative au-delà de la laideur. Il faudra des années avant que je trouve cela lisible. Doit être une meilleure façon.

Notez que les méthodes findChildren renvoient des tableaux qui fonctionnent bien sûr avec la syntaxe for (N n: array), mais pas avec la nouvelle Iterable.forEach méthode. J'ai dû 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 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 d'octet. Ça doit être une meilleure façon.

1
David Blevins