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

10

Java 8 ha aggiunto il concetto di interfacce funzionali , così come numerosi nuovi metodi che sono progettati per prendere interfacce funzionali. Le istanze di queste interfacce possono essere create in modo succinto usando espressioni di riferimento al metodo (ad esempio SomeClass::someMethod ) e espressioni lambda (es. (x, y) -> x + y ).

Un collega e io abbiamo opinioni divergenti su quando è meglio usare una forma o l'altra (dove "il migliore" in questo caso si riduce davvero a "più leggibile" e "più in linea con le pratiche standard", come loro " fondamentalmente altrimenti equivalente). In particolare, questo riguarda il caso in cui i seguenti sono tutti veri:

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

La mia opinione corrente sulla questione è che l'aggiunta di un metodo privato e il riferimento al metodo di riferimento sono l'approccio migliore. Sembra che questo sia il modo in cui la funzione è stata progettata per essere utilizzata e sembra più semplice comunicare ciò che sta accadendo tramite i nomi dei metodi e le firme (ad esempio "boolean isResultInFuture (risultato risultato)" sta chiaramente dicendo che sta restituendo un valore booleano). Rende inoltre più riutilizzabile il metodo privato se un miglioramento futuro della classe vuole utilizzare lo stesso controllo, ma non ha bisogno del wrapper dell'interfaccia funzionale.

La preferenza del mio collega è di avere un metodo che restituisce l'istanza dell'interfaccia (ad esempio "Predicate resultInFuture ()"). Per me, sembra che non sia proprio il modo in cui la funzione è stata pensata per essere usata, si sente leggermente più clamorosa e sembra che sia più difficile comunicare realmente l'intento 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 gli intenti dei designer linguistici o più leggibile? Escludendo una fonte ufficiale, ci sono motivi evidenti per cui uno sarebbe l'approccio migliore?

    
posta M. Justin 03.11.2016 - 20:49
fonte

3 risposte

8

In termini di programmazione funzionale, ciò di cui tu e il tuo collega state parlando è point free style , in particolare eta reduction . Considera i seguenti due compiti:

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

Questi sono equivalenti dal punto di vista operativo, e il primo è chiamato stile pointful mentre il secondo è point free style. Il termine "punto" si riferisce ad un argomento di funzione con nome (questo deriva dalla topologia) che manca nel secondo caso.

Più in generale, per tutte le funzioni F, un wrada lambda che prende tutti gli argomenti dati e li passa a F invariato, quindi restituisce il risultato di F invariato è identico a F stesso. Rimuovendo il lambda e usando F direttamente (passando dallo stile puntuale allo stile point-free di cui sopra) si parla di eta reduction , un termine che deriva da calcolo lambda .

Ci sono espressioni senza punti che non sono create dalla riduzione di eta, il classico esempio di ciò è 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));
}

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

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

Sebbene l'implementazione di compose usi stile puntuale con lambdas, il uso di comporre per definire isFoosResultInFuture è privo di punti perché non è necessario menzionare argomenti.

La programmazione point free è anche chiamata programmazione tacit e può essere un potente strumento per aumentare la leggibilità rimuovendo dettagli privi di punteggiatura da una definizione di funzione. Java 8 non supporta la programmazione point free quasi tanto quanto i linguaggi più funzionali come Haskell, ma una buona regola empirica è di eseguire sempre la riduzione dell'etá. Non è necessario utilizzare lambda che non hanno un comportamento diverso dalle funzioni che avvolgono.

    
risposta data 03.11.2016 - 21:26
fonte
4

Nessuna delle risposte attuali in realtà affronta il nucleo della domanda, che è se la classe debba avere un metodo private boolean isResultInFuture(Result result) o un metodo private Predicate<Result> resultInFuture() . Mentre sono 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 l'unità esegue il test 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 in seguito cambi la classe in modo che tu chiami il metodo direttamente, questo disaccoppiamento ti ripagherà in un modo molto piccolo.

Il primo approccio è anche superiore se si desideri adattare questo metodo a diverse interfacce funzionali oa 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 dà la flessibilità necessaria.

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

In definitiva la decisione è soggettiva. Ma se non intendi chiamare i metodi su Predicato, preferirei il primo approccio.

    
risposta data 11.02.2017 - 23:26
fonte
3

Non scrivo più codice Java, ma scrivo in un linguaggio funzionale per vivere e sostengo anche altri team che stanno imparando la programmazione funzionale. Lambdas ha un punto delicato e delicato di leggibilità. Lo stile generalmente accettato per loro è che li usi quando li puoi allineare, ma se senti la necessità di assegnarlo a una variabile per utilizzarlo in un secondo momento, dovresti probabilmente definire un metodo.

Sì, le persone assegnano lambdas alle variabili nei tutorial in ogni momento. Questi sono solo tutorial per aiutarti a capire a fondo come funzionano. In realtà non vengono utilizzati in questo modo, tranne in circostanze relativamente rare (come la scelta tra due lambda usando un'espressione if).

Questo significa che se il tuo lambda è abbastanza corto per essere facilmente leggibile dopo la fase di inlining, come nell'esempio del commento di @JimmyJames, allora vai a prenderlo:

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

Confronta questo aspetto 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 con un metodo chiamato a causa della sua lunghezza. Java è un linguaggio fastidiosamente prolisso, quindi forse nel tempo le sue pratiche specifiche della lingua saranno diverse dalle altre lingue, ma fino ad allora, mantieni il tuo lambda corto e in linea.

    
risposta data 03.11.2016 - 21:47
fonte

Leggi altre domande sui tag