In Java 8, è stilisticamente meglio usare espressioni di riferimento o metodi che restituiscono un'implementazione dell'interfaccia funzionale?


11

Java 8 ha aggiunto il concetto di interfacce funzionali , nonché numerosi nuovi metodi progettati per assumere interfacce funzionali. Le istanze di queste interfacce possono essere create in modo succinto usando espressioni di riferimento del metodo (ad es. SomeClass::someMethod) Ed espressioni lambda (ad es (x, y) -> x + y.).

Un collega e io abbiamo opinioni diverse su quando sia meglio usare una forma o l'altra (dove "migliore" in questo caso si riduce davvero a "più leggibile" e "la maggior parte in linea con le pratiche standard", dato che sostanzialmente sono altrimenti equivalente). Nello specifico, ciò implica il caso in cui sono vere tutte le seguenti condizioni:

  • la funzione in questione non viene utilizzata al di fuori di un singolo ambito
  • dare un nome all'istanza aiuta la leggibilità (al contrario, ad esempio, della logica è abbastanza semplice da vedere cosa sta succedendo a colpo d'occhio)
  • non ci sono altri motivi di programmazione per cui un modulo sarebbe preferibile all'altro.

La mia attuale opinione in merito è che l'aggiunta di un metodo privato e il riferimento a tale metodo sia l'approccio migliore. Sembra che sia così che la funzionalità è stata progettata per essere utilizzata e sembra più facile comunicare cosa sta succedendo tramite nomi e firme dei metodi (ad es. "Booleano isResultInFuture (Risultato del risultato)" sta chiaramente dicendo che sta restituendo un valore booleano). Inoltre, rende il metodo privato più riutilizzabile se un miglioramento futuro della classe desidera utilizzare lo stesso controllo, ma non necessita del wrapper di interfaccia funzionale.

La preferenza del mio collega è di avere un metodo che restituisca l'istanza dell'interfaccia (es. "Predicate resultInFuture ()"). Per me, sembra che non sia proprio il modo in cui la funzionalità è stata progettata per essere utilizzata, sembra leggermente più clunkier e sembra che sia più difficile comunicare realmente l'intenzione attraverso la denominazione.

Per rendere concreto questo esempio, ecco lo stesso codice, scritto nei diversi stili:

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    results.filter(this::isResultInFuture).forEach({ result ->
      // Do something important with each future result line
    });
  }

  private boolean isResultInFuture(Result result) {
    someOtherService.getResultDateFromDatabase(result).after(new Date());
  }
}

vs.

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    results.filter(resultInFuture()).forEach({ result ->
      // Do something important with each future result line
    });
  }

  private Predicate<Result> resultInFuture() {
    return result -> someOtherService.getResultDateFromDatabase(result).after(new Date());
  }
}

vs.

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    Predicate<Result> resultInFuture = result -> someOtherService.getResultDateFromDatabase(result).after(new Date());

    results.filter(resultInFuture).forEach({ result ->
      // Do something important with each future result line
    });
  }
}

Esistono documenti o commenti ufficiali o semi-ufficiali sul fatto che un approccio sia più preferito, più in linea con le intenzioni dei progettisti del linguaggio o più leggibile? Escludendo una fonte ufficiale, ci sono dei motivi chiari per cui uno sarebbe l'approccio migliore?


1
I lambda sono stati aggiunti alla lingua per un motivo e non per peggiorare Java.

@Snowman Questo è ovvio. Suppongo quindi che ci sia una relazione implicita tra il tuo commento e la mia domanda che non sto prendendo abbastanza. Qual è il punto che stai cercando di chiarire?
M. Justin,

2
Suppongo che il punto che sta sottolineando sia che, se Lambdas non avesse migliorato lo status quo, non si sarebbero preoccupati di presentarli.
Robert Harvey,

2
@RobertHarvey Certo. A quel punto, tuttavia, vennero aggiunti contemporaneamente lambda e riferimenti al metodo, quindi nessuno dei due era status quo prima. Anche senza questo caso particolare, ci sono momenti in cui i riferimenti ai metodi sarebbero ancora migliori; ad esempio, un metodo pubblico esistente (ad esempio Object::toString). Quindi la mia domanda è più se è meglio in questo particolare tipo di istanza che sto ponendo qui, che se esistono casi in cui uno è migliore dell'altro, o viceversa.
M. Justin,

Risposte:


8

In termini di programmazione funzionale, ciò di cui tu e il tuo collega state discutendo è uno stile senza punti , più specificamente riduzione dell'eta . Considera i seguenti due compiti:

Predicate<Result> pred = result -> this.isResultInFuture(result);
Predicate<Result> pred = this::isResultInFuture;

Questi sono funzionalmente equivalenti e il primo è chiamato stile puntuale mentre il secondo è stile senza punti . Il termine "punto" si riferisce a un argomento di funzione denominato (questo deriva dalla topologia) che manca in quest'ultimo caso.

Più in generale, per tutte le funzioni F, un lambda wrapper che accetta tutti gli argomenti forniti e li passa a F invariato, quindi restituisce il risultato di F invariato è identico a F stesso. La rimozione della lambda e l'uso diretto di F (passando dallo stile puntuale allo stile senza punti sopra) è chiamato riduzione dell'eta , un termine che deriva dal calcolo lambda .

Esistono espressioni senza punti non create dalla riduzione dell'eta, il cui esempio più classico è la composizione di funzioni . Data una funzione dal tipo A al tipo B e una funzione dal tipo B al tipo C, possiamo comporli insieme in una funzione dal tipo A al tipo C:

public static Function<A, C> compose(Function<A, B> first, Function<B, C> second) {
  return (value) -> second(first(value));
}

Supponiamo ora di avere un metodo Result deriveResult(Foo foo). Poiché un predicato è una funzione, possiamo costruire un nuovo predicato che prima chiama deriveResulte quindi verifica il risultato derivato:

Predicate<Foo> isFoosResultInFuture =
    compose(this::deriveResult, this::isResultInFuture);

Sebbene l' implementazione di composeusi uno stile puntuale con lambdas, l' uso di compose per definire isFoosResultInFutureè privo di punti perché non è necessario menzionare alcun argomento.

La programmazione senza punti è anche chiamata programmazione tacita e può essere un potente strumento per aumentare la leggibilità rimuovendo i dettagli inutili (giochi di parole) da una definizione di funzione. Java 8 non supporta la programmazione senza punti quasi completamente quanto i linguaggi più funzionali come fanno Haskell, ma una buona regola empirica è quella di eseguire sempre la riduzione dell'et. Non è necessario utilizzare lambda che non hanno un comportamento diverso dalle funzioni che avvolgono.


1
Penso che ciò di cui io e il mio collega stiamo discutendo siano più questi due compiti: Predicate<Result> pred = this::isResultInFuture;e Predicate<Result> pred = resultInFuture();dove resultInFuture () restituisce a Predicate<Result>. Quindi, sebbene questa sia una grande spiegazione di quando usare il riferimento al metodo rispetto a un'espressione lambda, non copre del tutto questo terzo caso.
M. Justin,

4
@ M.Justin: il resultInFuture();compito è identico al compito lambda che ho presentato, tranne nel mio caso il tuo resultInFuture()metodo è in linea. Quindi questo approccio ha due livelli di riferimento indiretto (nessuno dei quali fa nulla): il metodo di costruzione lambda e lo stesso lambda. Persino peggio!
Jack,

5

Nessuna delle risposte attuali affronta effettivamente il nocciolo della domanda, ovvero se la classe debba avere un private boolean isResultInFuture(Result result)metodo o un private Predicate<Result> resultInFuture()metodo. Sebbene siano in gran parte equivalenti, ci sono vantaggi e svantaggi per ciascuno.

Il primo approccio è più naturale se si chiama il metodo direttamente oltre a creare un riferimento al metodo. Ad esempio, se si esegue il test unitario di questo metodo, potrebbe essere più semplice utilizzare il primo approccio. Un altro vantaggio del primo approccio è che il metodo non deve preoccuparsi di come viene utilizzato, disaccoppiandolo dal suo sito di chiamata. Se successivamente cambi la classe in modo da chiamare direttamente il metodo, questo disaccoppiamento pagherà in modo molto piccolo.

Il primo approccio è anche superiore se si desidera adattare questo metodo a diverse interfacce funzionali o differenti unioni di interfacce. Ad esempio, potresti volere che il riferimento al metodo sia di tipo Predicate<Result> & Serializable, che il secondo approccio non ti offre la flessibilità di ottenere.

D'altro canto, il secondo approccio è più naturale se si intende eseguire operazioni di ordine superiore sul predicato. Predicate ha diversi metodi su di esso che non possono essere chiamati direttamente su un riferimento di metodo. Se si intende utilizzare questi metodi, il secondo approccio è superiore.

Alla fine la decisione è soggettiva. Ma se non hai intenzione di chiamare metodi su Predicate, mi spingerei a preferire il primo approccio.


Le operazioni di ordine superiore sul predicato sono ancora disponibili lanciando il riferimento del metodo:((Predicate<Resutl>) this::isResultInFuture).whatever(...)
Jack

4

Non scrivo più codice Java, ma scrivo in un linguaggio funzionale per vivere e supporto anche altri team che stanno imparando la programmazione funzionale. Le lambda hanno un delicato punto dolce di leggibilità. Lo stile generalmente accettato per loro è che li usi quando puoi incorporarli, ma se senti la necessità di assegnarlo a una variabile da usare in seguito, probabilmente dovresti semplicemente definire un metodo.

Sì, le persone assegnano continuamente lambda alle variabili dei tutorial. Questi sono solo tutorial per aiutarti a capire a fondo come funzionano. In realtà non sono usati in quel modo nella pratica, tranne in circostanze relativamente rare (come scegliere tra due lambda usando un'espressione if).

Ciò significa che se la tua lambda è abbastanza corta da essere facilmente leggibile dopo l'inline, come nell'esempio del commento di @JimmyJames, allora provaci:

people = sort(people, (a,b) -> b.age() - a.age());

Confronta questo con la leggibilità quando provi a incorporare il tuo esempio lambda:

results.filter(result -> someOtherService.getResultDateFromDatabase(result).after(new Date()))...

Per il tuo esempio particolare, lo contrassegno personalmente su una richiesta pull e ti chiedo di estrarlo a un metodo denominato a causa della sua lunghezza. Java è un linguaggio fastidiosamente prolisso, quindi forse con il tempo le sue pratiche specifiche per la lingua differiranno dalle altre lingue, ma fino ad allora, manterrai i tuoi lambda brevi e in linea.


2
Ri: verbosità - Sebbene le nuove aggiunte funzionali non lo riducano, da sole riducono la verbosità, ma consentono modi per farlo. Ad esempio puoi definire: public static final <T> List<T> sort(List<T> list, Comparator<T> comparator)con l'implementazione ovvia e poi puoi scrivere codice come questo: people = sort(people, (a,b) -> b.age() - a.age());che è un grande miglioramento dell'IMO.
JimmyJames,

Questo è un ottimo esempio di ciò di cui stavo parlando, @JimmyJames. Quando le lambda sono corte e possono essere allineate, sono un grande miglioramento. Quando durano così tanto che vuoi separarli e dargli un nome, stai meglio solo definendo un metodo. Grazie per avermi aiutato a capire come la mia risposta è stata fraintesa.
Karl Bielefeldt,

Penso che la tua risposta sia andata bene. Non sono sicuro del motivo per cui sarebbe stato votato verso il basso.
JimmyJames,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.