/**
   POGL : programmation objet et génie logiciel
   Cours du 5 février 2018, lambda-expressions

   @author Thibaut Balabonski @ Université Paris-Sud
*/

import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
import java.util.function.*;

/**
   Passer une méthode en paramètre.
   On cherche à définir une méthode [forEach] attendant deux paramètres :
   - une collection d'objets de type [T], implémentant [Iterable<T>]
   - une méthode [void f(T elt)] à appliquer à chaque élément de la collection

   Comme on ne peut pas passer de méthode en paramètre à une autre méthode,
   on peut à la place utiliser un objet possédant la méthode [f] souhaitée.
   Nous définissons ci-dessous une interface [FProvider<T>] qui caractérise les
   classes fournissant une méthode [f] avec la bonne signature.
*/

interface FProvider<T> {
    void f(T elt);
}

class ForEach {
    public static <T> void forEach(Iterable<T> c, FProvider<T> fProvider) {
	Iterator<T> it = c.iterator();
	while (it.hasNext()) {
	    T elt = it.next();
	    fProvider.f(elt);
	}
    }
    /**
       Remarque : les structures implémentant l'interface [Iterable] donnent
       accès à la deuxième forme des boucles [for] de [Java]. La définition
       suivante est équivalente à la première (plus précisément, la suivante
       est traduite en la première).
    public static <T> void forEach(Iterable<T> c, FProvider<T> fProvider) {
	for(T elt : c) {
	    fProvider.f(elt);
	}
    }
    */
}

/**
   Pour utiliser cette méthode [forEach], il faut encore :
   - définir une classe fournissant la méthode [f] voulue, et donc implémentant
     l'interface [FProvider],
   - créer un nouvel objet de cette classe, à passer en paramètre à [forEach].
*/

class Printer implements FProvider<String> {
    public void f(String s) {
	// Ici, au milieu de la définition de classe,
	// gît la seule ligne de code réellement utile.
	System.out.print(s);
    }
}

class FProviderTest {
    public static void main(String[] args) {
	List<String> c = new ArrayList<>();
	c.add("À l'origine fut la vitesse,");
	c.add("le pur mouvement furtif,");
	c.add("le << vent-foudre >>.\n");
	FProvider<String> printer = new Printer();
	ForEach.forEach(c, printer);
	// Sans donner de nom à l'objet [printer], les deux lignes
	// précédentes auraient également pu être réduites à
	//   ForEach.forEach(c, new Printer())
    }
}

/**
   Définir une classe dans l'unique but de fournir une méthode à usage
   unique peut paraître dommage. Plus grave, cela éloigne la définition
   de la méthode [f] de son utilisation, ce qui obscurcit la présentation.

   Avant Java 8, on pouvait faire un peu mieux avec le mécanisme des
   classes anonymes, mais cela reste lourd. Depuis Java 8, les
   lambda-expressions donnent une solution plus satisfaisante.

   Notre appel à [forEach] peut être réduit à la ligne suivante :
     ForEach.forEach(c, (String s) -> { System.out.print(s); })

   Ce code très compact réalise à la fois :
   - la définition de la méthode [f], sans nommer cette méthode ni même
     la classe à laquelle elle appartient,
   - la création de l'objet fournissant la méthode [f]
   
   La syntaxe des lambda-expressions est en trois parties :
   1/ Un n-uplet de paramètres avec leurs types, comme
          (String s)
      ou
          (int n, boolean b)
   2/ Le symbole flèche
          ->
   3/ Un bloc de code entre accolades
          { ... }
*/

class ForEachLambdaTest {
    public static void main(String[] args) {
	List<String> c = new ArrayList<>();
	c.add("À l'origine fut la vitesse,");
	c.add("le pur mouvement furtif,");
	c.add("le << vent-foudre >>.\n");
	ForEach.forEach(c, (String s) -> { System.out.print(s); });
	ForEach.forEach(c, (String s) -> {
		String vent = s.replaceAll("[a-zÀ]", " ");
		System.out.print(vent);
	    });
    }
}

/**
   Petites simplifications possibles :
   - Il n'est généralement pas nécessaire de préciser les types des paramètres,
     qui peuvent être déduits du contexte (inférence de type, ou en Java
     "context typing").
   - Si le bloc de code est réduit à :
         { return e; }
     pour une certaine expression [e], on peut n'écrire que
         e

   Autrement dit, la lambda-expression
       (int i) -> { return i*i; }
   sera généralement écrite
       i -> i*i
*/

/**
   Une lambda-expression peut être reconnue comme instanciant n'importe quelle
   interface fonctionnelle, pour peu qu'elle soit cohérente avec le type de
   l'unique méthode abstraite de cette interface.
   Le nom de la méthode n'a aucune importance, et elle automatiquement définie
   par la lambda-expression.
   On a par exemple les interfaces fonctionnelles suivantes dans le paquet
   java.util.function de la bibliothèque standard.
*/

// Consomme un paramètre de type [T], sans renvoyer de résultat
interface Consumer<T> {
    void accept(T elt);
}

// Prend un paramètre de type [T] et renvoie un résultat de type [R]
interface Function<T, R> {
    R apply(T elt);
}

// Teste un paramètre de type [T] et renvoie un booléen
interface Predicate<T> {
    boolean test(T elt);
}

class LambdaTest {
    public static void main(String[] args) {
	// Définition d'une variable de type [Predicate<Integer>] par une
	// lambda-expression
	Predicate<Integer> even = i -> i%2 == 0;
	// La méthode [test] a été définie automatiquement
	System.out.println(even.test(3));
	// Remarquez que la même lambda-expression aurait pu instancier un
	// autre type
	Function<Integer, Boolean> stillEven = i -> i%2 == 0;
	// Dans ce cas c'est la méthode [apply] qui est définie
	System.out.println(stillEven.apply(3));
	// La commande suivante aurait été rejetée, car la méthode [test]
	// n'existe pas pour [stillEven] de type [Function].
	// System.out.println(stillEven.test(3));
    }
}

/**
   Pointeurs de méthodes

   Les lambda-expressions permettent d'écrire des expressions qui représentent
   des méthodes anonymes. On peut de même écrire des expressions représentant
   des méthodes définies dans une classe avec la notation
       Class::method
 */

class PointerMethodTest {
    public static void main(String[] args) {
	List<String> c = new ArrayList<>();
	c.add("À l'origine fut la vitesse,");
	c.add("le pur mouvement furtif,");
	c.add("le << vent-foudre >>.\n");
	ForEach.forEach(c, System.out::print);	
    }
}