2. Programmazione funzionale

Interfacce Funzionali

Partiamo con un esempio:

class Person{
	private String name;
	private int age;
	...
	int getAge(){
		return age;
	}
}


List<Person> list = ...
list.add(...);

Ho una lista di persone: come posso ordinarla in modo tale da avere le persone ordinate in base alla data di nascita?

Posso usare il metodo Collections.sort(), che è definito come public static <T> void sort(List <T> list, Comparator <? super T> c).

A questo punto la lista ce l’ho, devo solo definire il comparator:

MyComparator c = new MyComparator(); 
Collections.sort(list, c);


class MyComparator implements Comparator<Person> { 
	@Override
	public int compare(Person p1, Person p2) {
	if (p1.getEta() < p2.getEta()) return -1; 
	else if (p1.getEta() > p2.getEta()) return 1; 
	else return 0;
	}
}

La classe MyComparator implementa una singola funzione che codifica il significato di fare il confronto tra due elementi: possiamo dire che il metodo sort() è parametrico rispetto alla funzione di confronto e che MyComparator rappresenta questa funzione.

Possiamo usare anche una classe anonima, invece di definire la classe MyComparator.

Collections.sort(list, new Comparator<Person>()) {//Anonymous class: 
	@Override
	public int compare(Person p1, Person p2) {
		if (p1.getEta() < p2.getEta()) return -1; 
		else if (p1.getEta() > p2.getEta()) return 1; 
		return 0;
	}
}

Le interfacce con un solo metodo, come Comparator, vengono dette interfacce funzionali.

Espressioni Lambda

In Java è anche possibile usare le espressioni lambda per definire il metodo di un’interfaccia funzionale.

Riprendendo l’esempio di prima, posso direttamente passare una funzione lambda al metodo Collections.sort().

Collections.sort(list, (p1, p2) -> {
	if (p1.getEta() < p2.getEta()) return -1; 
	else if (p1.getEta() > p2.getEta()) return 1; 
	else return 0;
});

Sintassi delle espressioni lambda

Le espressioni lambda possono essere scritte specificando il tipo di dato dei parametri della funzione, ad esempio:

obj.method(String name -> name.doSomething());
// Oppure
obj.methos(final String name -> name.doSomething());

Oppure possono essere scritte senza il tipo, lasciando l’operazione di inferenza di tipo al compilatore:

obj.method(name -> name.doSomething());
obg.method(final name -> name.doSomething()); // NO!! Questo è illegale

Comunque venga scritta l’espressione lambda, l’input è immutabile.

I valori di ritorno possono essere impliciti nel caso in cui la funzione esegua solo un’operazione:

obj.method((par1, par2) -> par1.foo(par2)); // È come se ci fosse scritto return par1.foo(par2)

Nel caso in cui la funzione esegua più di un’operazione è necessario specificare il valore di ritorno.

obj.doSomething(final String name -> { 
	if (name.equals("Boo"))
		return "Foo"; 
	else
	return name; });

Thread in programmazione funzionale

// Versione con la classe anonima
Runnable r = new Runnable(){
	@Override
	public void run(){
		doSomething();
	}
}

Thread t = new Thread(r);
t.start();
// Versione con l'espressione lambda
Runnable r = () -> doSomething();
Thread t = new Thread(r);
t.start();
// Oppure
Thread t = new Thread(() -> doSomething());
t.start();

Optional<T>

Il tipo Optional<T> permette di fare operazioni su dati, ignorando il fatto che alcuni dati potrebbero essere assenti (null).

Per creare un opzionale si può usare uno dei seguenti metodi:

public class Address {
	final String via;
	final Optional<String> comments;

	public Address(String street, String comments) {
		this.via = street;
		this.comments = Optional.of(comments); //creates an Optional with the value of comments
	}

	public Address(String via) {
		this.via = via;
		this.comments = Optional.empty(); //creates an empty Optional. i.e., replaces comments=null;
	}

	...
	public Optional<String> getComments() {
		return comments; 
	}
}

Come accedo al valore di un opzionale Optional<T> o?

Stream<T>

Le stream permettono di concatenare funzioni che agiscono su collezioni.

A partire da una Collection<T>, il metodo stream() ritorna una Stream<T>.

Metodo count(): conta gli elementi dello stream.

Metodo forEach(): prende in input una funzione che fa qualcosa con ogni elemento dello stream. Generalmente si usa per stampare le stream.

List<String> list = ...
list.stream()
		.forEach(x -> System.out.println(x));
// Oppure
list.stream()
    .forEach(System.out::println);

Metodo filter(): prende in input un predicato e ritorna una stream che contiene solo gli elementi per i quali il predicato è vero.

List<Integer> list = ...
list.stream()
    .filter(x -> x%2 == 0)
		.forEach(x -> System.out.println(x));

Metodo map(): prende in input una funzione che accetta il tipo T e la applica ad ogni elemento, ritornando una Stream<U>.

List<String> list = ...
list.stream()
    .map(x -> x.size())
    .forEach(x -> System.out.println(x));

Metodo distinct(): elimina i duplicati da uno stream

Metodo sort(): ordina gli elementi di uno stream; usa l’ordine naturale per variabili intere e stringhe, ha bisogno di un Comparator negli altri casi.

Metodo flatMap(): prende in input una funzione che accetta il tipo T, la applica a tutti gli elementi dello stream, producendo una Stream<U> per ogni elemento, e infine unisce tutte le Steam<U>.

Metodo limit(): prende in input un numero e ritorna una stream formata da i primi x elementi della stream originale.

Metodo sum(): fa la somma di tutti gli elementi di una stream di interi

Metodo average(): fa la media di tutti gli elementi di una stream di interi

Metodo reduce(): permette di operare una riduzione, cioè un’operazione che raccoglie un valore unico da una stream. I metodi per ricavare la somma e la media sono esempi di riduzione. È possibile creare metodi di riduzione custom, fornendo alla funzione un punto di partenza e l’operazione da eseguire ad ogni step.

Integer sum = list
	.stream()
	.flatMap(x -> x.size())
	.reduce(0, (a, b) -> a + b);
					

Metodo collect(): simile a reduce, prende in input tre funzioni: supplier, accumulatore e combinatore.

List< String> sentence = ... 
List<String> wordsConA =
	sentence.stream()
					.filter(word -> word.startsWith("a")) 
					.collect(ArrayList::new,
									 ArrayList::add, 
									 ArrayList::addAll);

Metodo parallel(): consente di far eseguire le operazioni sulle stream in parallelo.

java.utils.function

Function<T,R>: funzione che ha come parametro il tipo T e come ritorno il tipo R

BiFunction<T, U, R>: funzione che ha come parametri i tipi T ed U e come ritorno il tipo R

Consumer<T>: funzione che ha come parametro il tipo T e non ha valori di ritorno

Predicate<T>: funzione che ha come parametro il tipo T e ha un booleano come valore di ritorno

Chiusure

Supponiamo di voler usare un’espressione lambda più volte: è possibile usare un Predicate<T>, ad esempio:

Predicate<String> pred = s -> s.startsWith("L");
...
list.stream()
    .filter(pred)
		.forEach(x -> System.out.println(x));

Ma se volessi generalizzare l’esempio in modo tale che il predicato ci dica quali sono le stringhe che iniziano con un generico prefisso?

Posso creare un metodo che ritorna un’espressione lambda parametrizzata:

public static Predicate<String> pred(String prefix){
	return s -> s.startsWith(prefix);
}
...
list.stream()
		.filter(pred("X"))
		.forEach(x -> System.out.println(x));

L’espressione lambda in questo caso viene detta chiusura.

Esempi di codici con le stream

1) Codice per trovare il valore massimo di una lista di interi.

int max = prices.stream()
								.reduce(0, Math:max)

2) Codice che ritorna una lista formata da tutti i nomi che non erano completamente in maiuscolo, in maiuscolo. (Focalizzarsi sulla collect)

public static List<String> inUpperCase(List<String> people) { 
	return people.stream()
							 .filter(x -> !(x.equals(x.toUpperCase())))
							 .map(x -> x.toUpperCase())
							 .collect(Collectors.toList());
}