Introduzione alla programmazione funzionale

Ora che abbiamo visto e ripassato un pò le interfacce in java, è ora di iniziare a parlare di programmazione funzionale, introdotta con Java 8 per mezzo delle lambda expression.

Alla base della programmazione funzionale di java ci sono le interfacce funzionali.

Cos’è un interfaccia funzionale ? è semplice, si tratta di un interfaccia che definisce un solo metodo astratto.

definizione di un interfaccia funzionale

Vediamo come è possibile definire in java un interfaccia che sia di fatto un interfaccia funzionale :

@FunctionalInterface
public interface Decolla {
  public int getV1Velocity();
}
public class Embraer implements Decolla {
  public int getV1Velocity() {
    return 120;
  }
}

Questo semplice esempio definisce un interfaccia funzionale poichè contiene solo 1 metodo astratto getV1Velocity.

Possiamo notare l’utilizzo dell’annonation @FunctionalInterface, non ci facciamo trarre in inganno, l’utilizzo di questa annotation non è assolutamente obbligatorio, Java non controlla la presenza di questa annotation per stabilire se l’interfaccia è funzionale o no, ma come detto prima, lo è se e solo se definisce 1 solo metodo astratto.

A cosa serve allora questa annotation ? serve nel mondo reale, immagina di sviluppare del codice che utilizza una tua interfaccia funzionale, un domani, un tuo collega, potrebbe modificare la tua interfaccia aggiungendovi un metodo, e tutto il codice che hai sviluppato smetterà di funzionare perchè questa non sarà più un interfaccia funzionale, se avessi utilizzato l’annotation, avresti informato il tuo collega che quella è e deve restare un interfaccia funzionale.

Ora stiamo attenti, vediamo degli esempio di interfacce e cerchiamo di riconoscere quelle funzionali da quelle non:

public interface DecollaVerticale extends Decolla { }
public interface DecollaSuAcqua extends Decolla {
  public int getV1Velocity(); 
}
public interface DecollaDaPortaerei extends Decolla {
  public default int getDistanzaMinima() { return 100; }
  public static void postBruciatori() { System.out.println("postbruciatori on"); }
}

Tutte e 3 queste interfacce sono interfacce funzionali, la prima estende Decolla che è funzionale e non aggiunge nessun metodo, quindi resta valida

la seconda estende Decolla ma ridefinisce getV1Velocity di fatto, l’interfaccia risultate contiene sempre 1 solo metodo astratto

La terza ed ultima, aggiunge 1 metodo default e 1 metodo statico, ma di fatto, resta con 1 metodo astratto, quindi funzionale

Implementare la programmazione funzionale con le lambda expression

Ora che abbiamo definito un interfaccia funzionale, vediamo come implementarla mediante l’uso delle istruzioni lambda.

Come sappiamo già, un instruzione lambda è un blocco di codice che può essere passato, come un metodo anonimo, iniziamo definendo un interfaccia funzionale CheckAereoplane che definisce 1 metodo test.

public interface CheckAereoplane {
  public boolean test(Aereo a);
}
public class Aereo {
  private String costruttore;
  private boolean canLandOnWater;
  private boolean canLandOnGrass;
  public Aereo(String costruttore, boolean canLandOnWater, boolean canLandOnGrass) {
    this.costruttore = costruttore;
    this.canLandOnWater = canLandOnWater;
    this.canLandOnGrass = canLandOnGrass;
  }
  public boolean canLandOnWater() { return this.canLandOnWater; }
  public boolean canLandOnGrass() { return this.canLandOnGrass; }
}

Ora abbiamo definito la struttura, scriviamo un semplice programma che controlla se il nostro aereo soddisfa alcuni criteri e lo faremo con le lambda expression:

public class AereoTester {
  private static void stampa(Aereo aereo,CheckAereoplane checkAereoplane) {
    if (checkAereoplane.test(aereo))
      System.out.println(aereo);
  }
  public static void main(String... args) {
    stampa(new Aereo("Boeing747",false,false), a -> a.canLandOnGrass());
    stampa(new Aereo("Cessna",false,true), a -> a.canLandOnGrass());
  }
}

qui vediamo l’utilizzo dell’istruzione lambda

a -> a.conLandOnGrass()

Di fatto, stiamo passando il corpo del metodo test dell’interfaccia funzionale CheckAereoplane, Java sa che la nostra intefaccia vuole in input un Aereo e di conseguenza assumerà che a è un Aereo.

Comprendere la sintassi di un istruzione lambda

Qui bisogna fare attenzione, la sintassi delle istruzioni lambda è fuorviante poichè molte cose sono opzionali, vediamo quello che abbiamo scritto prima :

a -> a.conLandOnGrass()

che potrebbe essere scritta come :

(Animal a) - > { return a.canLandOnGrass(); }

A sinistra dell’operatore -> abbiamo la lista dei parametri di input dell’istruzione lambda, la parte a destra è il corpo dell’espressione.

Vediamo le differenze tra le due sintassi, la prima omette le parentesi per racchiudere i parametri in input, le parentesi possono essere omesse se e solo se vi è 1 parametro e non è specificato il tipo 

queste sono lambda corrette :

() -> new Md80() //0 parametri parentesi richieste
d -> { return d.decolla(); } // 1 parametro senza tipo, parentesi omesse
(Aereo a) -> a.decolla() // tipo specificato ci vogliono le parentesi

Queste invece sono invalide :

Aereo a -> a.decolla() //Tipo specificato senza le parentesi
a, b -> a.sorpassa(b); // 2 parametri senza parentesi
Aereo a, Torre t -> a.comunica(t); // due parametri senza parentesi

Prestiamo attenzione anche alla parte destra, come per la lista parametri, se il corpo è composto da 1 sola istruzione possiamo omettere le parentesi graffe e il punto e virgola e l’istruzione return.

() -> true 
a -> { return a.startWith("test"); } 
(String a) -> a.startWith("test");
(int x) -> {}
(int y) -> {return;}

Queste sono tutte valide, notiamo che alcune sono miste, omettono parti a destra o a sinistra.

Continuiamo con altri esempi :

(a,b) -> a.startsWith(b) // 1
(String a, String b) -> a.starstWith(b)

Queste istruzioni hanno due parametri in input e il corpo è formato da 1 sola istruzione, vediamo che nella prima, abbiamo omesso il tipo ma abbiamo dovuto mettere le parentesi, se omettiamo il tipo lo dobbiamo omettere per tutti non possiamo fare una cosa mista tipo :

(a, String b) -> a.startsWith(b) //errore

Nella seconda abbiamo specificato il tipo per entrambi.

Vediamo ancora alcuni esempi di lambda errate :

a,b -> a.startsWith(b) 
(String a, String b) -> { a.startsWith(b); }
(a,b) -> { return a.startsWith(b) }

Nella prima, visto che abbiamo due parametri ci siamo scordati le parentesi, nella seconda, visto che abbiamo inserito le parentesi graffe, abbiamo omesso l’istruzione return, nella terza, che sembra completa, in realtà manca il ; alla fine.

Un ultima cosa da tenere sempre a mente nelle istruzioni lambda, non possiamo ridichiarare nomi di variabili utilizzati nella lista dei parametri, ad esempio :

(a,b) -> { int a = 5; return a*b; }

Vediamo che nel corpo dell’istruzione abbiamo definito una variabile a, già utilizzata nell’elenco dei parametri.

L’interfaccia Predicate

Torniamo alla nostra interfaccia funzionale CheckAereoplane , l’abbiamo creata per testare un Aereo, questo implica che dovremmo fornire un interfaccia funzionale per ogni nostro tipo di dato, fortunatamente, Java ci viene in aiuto e ci fornisce un interfaccia adatta a questo scopo per tutte le classi, questa interfaccia si trova nel package java.util.function

public interface Predicate<T> {
  public boolean test(T t);
}

Se guardiamo con attenzione, scopriamo che è molto simile alla nostra interfaccia CheckAereoplane , tranne per il fatto che l’argomento è un generics, che vedremo bene in seguito.

possiamo quindi riscrivere il nostro programmino utilizzando ora questa interfaccia :

public class AereoTester {
  private static void stampa(Aereo aereo,Predicate<Aereo> checkAereoplane) {
    if (checkAereoplane.test(aereo))
      System.out.println(aereo);
  }
  public static void main(String... args) {
    stampa(new Aereo("Boeing747",false,false), a -> a.canLandOnGrass());
    stampa(new Aereo("Cessna",false,true), a -> a.canLandOnGrass());
  }
}

possiamo notare che l’unica cosa che cambia è il parametro al metodo stampa, ora utilizza l’interfaccia Predicate al posto di CheckAereoplane

Più avanti studieremo tutti i tipi di interfacce funzionali di Java 8 come Predicate, Supplier, Consumer, Function, BiFunction, BiConsumer, etc etc