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
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());
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.
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;
});
}
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.