Programmazione con Scala...

Preview:

Citation preview

Programmazione funzionale

Pistoia2017

con Scala

s.peruzzi@gmail.com

@gundam79

linkedin.com/in/stefanoperuzzi

La PF è una novità?

IPL, LISP(anni 50)

La PF è una novità?

IPL, LISP(anni 50)

ML, Scheme(anni 70)

La PF è una novità?

IPL, LISP(anni 50)

ML, Scheme(anni 70)

Haskell(anni 90)

La PF è una novità?

IPL, LISP(anni 50)

ML, Scheme(anni 70)

Haskell(anni 90)

OCaml

Erlang(Ericsson)

F#.NET

La PF è una novità?

IPL, LISP(anni 50)

ML, Scheme(anni 70)

Haskell(anni 90)

OCaml

Erlang

F#.NET

ScalaJVM

La PF nei linguaggi più diffusiCercando "functional" su manning.com e oreilly.com:

● Functional Programming in Java● Functional Programming in C#● Real-World Functional Programming (With examples in F# and C#)● Functional Programming in JavaScript● Functional Programming in C++● Functional Programming in Python● Functional Programming in PHP

Non è obbligatorio imparare un nuovo linguaggio per usare la PF!

Preparare l’ambiente Scala● Installare Java JDK

● Scaricare la distribuzione: http://www.scala-lang.org/download/

● Usare un editor con syntax highlighting, es.: Atom

● Ancora meglio: un IDE, consigliato https://www.jetbrains.com/idea/ con

plugin per Scala

● Provate la REPL, basta lanciare “scala” in una console

Cos’è la programmazione funzionale

Cos’è la PFPrincipio fondamentale della Programmazione Funzionale:

costruire programmi usando pure functions => functions without side effects, funzioni senza “effetti collaterali”.

Esempi di side effects:

● Modificare una variabile (globale) o una struttura dati● Lanciare un’eccezione● Leggere/scrivere un file

Cos’è una pure functionDefiniamo la funzione f: A => B (da valori di tipo A a valori di tipo B) una computazione che trasforma ogni valore a di tipo A in un valore b di tipo B.

Esempio di funzioni:

● length: String → Int ● +: (Int, Int) → Int

Se f è una funzione (pura) non avrà altri effetti (side effects) che tornare il suo risultato. Length e + sono funzioni pure.

Referential transparency e substitution modelUn'espressione (e in particolare una funzione) è referentially transparent se, per ogni programma p, sostituendo l’espressione con la sua valutazione non si modifica p.

● l’espressione (2 + 3) può essere sostituita con 5● Se x = sqrt(2), si può sostituire ogni occorrenza di x nel programma con

sqrt(2)

Definizione: una funzione f è pura se f(x) è referentially transparent per tutte le espressioni x a sua volta referentially transparent. In pratica, se anch’essa è sostituibile con la sua valutazione.

Esempio di referential transparencyIn Scala: val x = "Hello World" val r1 = x.reverse val r2 = x.reverse

→ r1 e r2 sono uguali

Possiamo applicare il principio di sostituzione: x → “Hello World” val x = "Hello World" val r1 = "Hello World".reverse val r2 = "Hello World".reverse

→ r1 e r2 sono uguali

Esempio di (not) referential transparency val x = new StringBuilder("Hello") val y = x.append(" World") val r1 = y.toString // r1 = "Hello World" val r2 = y.toString // r2 = "Hello World"

→ r1 e r2 sono uguali

Sostituzione: y → x.append(" World")

Esempio di (not) referential transparency val x = new StringBuilder("Hello") val y = x.append(" World") val r1 = y.toString // r1 = "Hello World" val r2 = y.toString // r2 = "Hello World"

→ r1 e r2 sono uguali

Sostituzione: y → x.append(" World")

val x = new StringBuilder("Hello") val r1 = x.append(" World").toString // r1 = "Hello World" val r2 = x.append(" World").toString // r2 = "Hello World World"→ r1 e r2 sono diversi!

La funzione append() di StringBuilder non è una pure function.

I benefici della referential transparencyVi sembra complicato? Perché è complicato!

Per lo più debugging implica ricostruire l’evoluzione dello stato di un programma.

Il substitution model è molto più facile!

I benefici della referential transparencyLe funzioni pure:

● separano la logica della computazione da “come si ottengono gli input” e da “cosa fare con gli output”

● permettono il local reasoning, perché la computazione non dipende dal contesto● sono più modulari ⇒ più facili da riutilizzare● sono facilmente testabili● sono componibili

E i cicli?

def factorial(n: Int): Int = { var result = 1

for(i <- Range(1,n)) { result = result * (i + 1) } result }

Un esempio: in scala “if” è una funzione che torna un risultato: la valutazione del ramo true o quella del ramo false. Al contrario i cicli non sono funzioni, non tornano un valore.

I cicli hanno intrinsecamente il concetto di stato (continuo il ciclo finché non succede qualcosa). Ad esempio:

Cicli → ricorsione

def factorial(n: Int): Int = {

def go(n: Int, acc: Int): Int = { if (n == 1) acc

else go(n-1, acc * n) }

go(n, 1) }

Fattoriale senza stato:

È più facile “da leggere”? Ma la ricorsione non era “il male”?!?

Tail recursion def factorial(n: Int): Int = {

@tailrec def go(n: Int, acc: Int): Int = { if (n == 1) acc

else go(n-1, acc * n) }

go(n, 1) }

Se l’ultima espressione di una funzione è la chiamata ricorsiva, il compilatore sa tradurre la ricorsione in un ciclo iterativo. Quindi no StackOverflowException.

Un esempio un po’ più completoImmaginiamo un semplice programma che

● Modellizza un caffè e un metodo di pagamento con carta di credito● Permette di comprare un caffè con la carta● Permette di comprare tanti caffè con la carta

Buy some Coffeeclass Coffee { val prezzo = 1.0}

class CartaCredito { def charge(p: Double) = { println(s"Pagato $p euro tramite carta di credito") }}

object App { /** * Program entry point. */ def main(args: Array[String]) {

... }}

Buy some Coffeeobject App {

def main(args: Array[String]) { val cc = new CartaCredito println("Result is " + buyCoffee(cc))

val coffees = buyCoffees(cc, 10) println(s"comprati ${coffees.length} caffe'") }

}

Buy some Coffee def buyCoffee(cc: CartaCredito): Coffee = { val coffee = new Coffee() cc.charge(coffee.prezzo)

coffee }

Buy some Coffee def buyCoffee(cc: CartaCredito): Coffee = { val coffee = new Coffee() cc.charge(coffee.prezzo)

coffee }

Domanda: è una funzione pura?

In altre parole: ha degli effetti collaterali?

def buyCoffee(cc: CartaCredito): Coffee = { val coffee = new Coffee() cc.charge(coffee.prezzo)

coffee }

def buyCoffees(cc: CartaCredito, n: Int): List[Coffee] = { // con var result e' ri-assegnabile var result = List[Coffee]()

for (i <- Range(0, n)) result = buyCoffee(cc) :: result

result }

Vediamo il progetto completo...

Un passo avantibuyCoffee() si occupa di preparare il caffè e pagare. Troppe cose per una sola funzione!

Riscriviamo in modo che:

● Prepari il caffé● Imposti il conto, senza pagarlo

E non compia altre “azioni” che non gli competono.

Per far questo introduciamo la classe Payment.

Buy some pure Coffeecase class Payment(cc: CartaCredito, amount: Double) { def pay() { cc.charge(amount) }}

/////

def buyCoffee(cc: CartaCredito): (Coffee, Payment) = { val coffee = new Coffee() val payment = Payment(cc, coffee.prezzo)

(coffee, payment) }

Una coppia di oggetti. Detto c questo dato, si accede ai singoli oggetti con c._1 e c._2

Buy some pure Coffee def main(args: Array[String]) { val cc = new CartaCredito val coffeePayment = buyCoffee(cc) println("Ho preso un " + coffeePayment._1) coffeePayment._2.pay()

val coffees = buyCoffees(cc, 10) println(s"comprati ${coffees.length} caffe', da pagare!") coffees.map(_._2.pay()) }

////

def buyCoffees(cc: CartaCredito, n: Int): List[(Coffee, Payment)]

Vediamo il progetto con l’introduzione di Payment

High Order FunctionsUn’altro concetto della PF è che le funzioni sono valori. Come ogni valore, le funzioni hanno un tipo. Ad esempio:

È una funzione con un argomento Int, e valore di ritorno Int. Si scrive così:

Questa funzione ha lo stesso tipo:

def factorial(n: Int): Int

Int => Int

def quadrato(n: Int): Int

High Order FunctionsPossiamo definire la funzione stampa:

Definizione: una high-order function è una funzione che accetta altre funzioni come argomento.

Analogamente si definiscono le funzioni come first-class value, valori "di prima classe". Come ogni altro valore possono essere passate come argomento o tornate come risultato.

def stampa(x: Int, f: Int => Int) = { println("Dato " + x + " il risultato e’ " + f(x)) }

Anonymous functionsNon è necessario dare un nome alla funzione, possiamo definirla in modo anonimo:

Si può evitare di specificare il tipo dell’argomento, se il compilatore è in grado di inferirlo dal contesto:

stampa(5, (x: Int) => x*7)

stampa(5, x => x/2)

Una funzione anonima con un argomento Int

Un altro esempio di function compositionVediamo la somma di interi da a a b:

La somma del quadrato di interi da a a b:

def sumInts(a: Int, b: Int): Int = if (a > b) 0 else a + sumInts(a + 1, b)

def square(x: Int): Int = x * x

def sumSquares(a: Int, b: Int): Int = if (a > b) 0 else square(a) + sumSquares(a + 1, b)

Un altro esempio di function compositionVediamo la somma delle potenze di 2 da a a b:

In pratica si calcola ∑ da a a b di una funzione f, con f = sum, square, powerOfTwo.

Possiamo riscrivere generalizzando f.

def powerOfTwo(x: Int): Int = if (x == 0) 1 else 2 * powerOfTwo(x - 1)

def sumPowersOfTwo(a: Int, b: Int): Int = if (a > b) 0 else powerOfTwo(a) + sumPowersOfTwo(a + 1, b)

HOF sum()Vediamo la somma di f(x) per x da a a b:

sum() è una high-order function, con essa possiamo riscrivere le funzioni precedenti:

def sum(f: Int => Int, a: Int, b: Int): Int = if (a > b) 0 else f(a) + sum(f, a + 1, b)

def id(x: Int): Int = xdef sumInts(a: Int, b: Int): Int = sum(id, a, b)def sumSquares(a: Int, b: Int): Int = sum(square, a, b)def sumPowersOfTwo(a: Int, b: Int): Int = sum(powerOfTwo, a, b)

sumInts è una funzione di tipo (Int, Int) => Int, come restituito da sum.

CurryingVediamo come distinguere l'intervallo (a, b) dalla funzione di somma:

In questa formulazione, sum ha come argomento una funzione e torna una nuova funzione sumF, ed è quest'ultima a prendere come parametro l'intervallo (a, b).

def sum(f: Int => Int): (Int, Int) => Int = { def sumF(a: Int, b: Int): Int = if (a > b) 0 else f(a) + sumF(a + 1, b) sumF}

def sumInts = sum(x => x)def sumSquares = sum(x => x * x)def sumPowersOfTwo = sum(powerOfTwo)

sumSquares(1, 10) + sumPowersOfTwo(10, 20)

CurryingÈ possibile invocare la somma in questi due modi analoghi:

Si definisce currying la tecnica che trasforma una funzione con più argomenti nell'invocazione di una catena di funzioni, ognuna con un sottoinsieme degli argomenti di partenza.

Fu definita da Moses Schonfinkel e riscoperta successivamente da Haskell Curry, da cui il nome.

sum(squares, 1, 10) = sum(square)(1, 10)

Collections & PF

ListLa classe List di Scala è una single linked list. È un buon esempio di struttura dati in cui sono usate le tecniche viste fino qui.

Nil è l'oggetto Lista vuota. List(...) è il metodo per costruire una List a partire dagli oggetti contenuti. :: pre-pende un oggetto, ++ unisce due List

val a = List[Int](1,2,3) val b = Nil val c = List() val d = List(3,7,1,10,4,9)

val e = 0 :: a

// lists are immutable: this does not compile! //a = 0 :: a

val f = a ++ d

Esempi con List: elaborazioneEseguiamo operazioni su tutti gli elementi della lista. È un lavoro per la ricorsione.Es.: somma degli elementi di una lista def sum(l: List[Int]): Int = ???

Esempi con List: elaborazioneEseguiamo operazioni su tutti gli elementi della lista. È un lavoro per la ricorsione.Es.: somma degli elementi di una lista

Domanda: e' una funzione tail recursive?

def sum(l: List[Int]): Int = { if (l.isEmpty) 0 else l.head + sum(l.tail) }

def sum(l: List[Int]): Int = { if (l.isEmpty) 0 else { println("sommo " + l.head) val r = l.head + sum(l.tail) println("risultato " + r) r }}

Esempi con ListProviamo a stampare qualcosa:

def sum(l: List[Int]): Int = { @annotation.tailrec def sumAcc(l: List[Int], sum: Int): Int = { if (l.isEmpty) sum else sumAcc(l.tail, sum + l.head) }

sumAcc(l, 0)}

Esempi con List: accumulatoreTrick: usiamo un accumulatore per evitare espressioni che coinvolgono la ricorsione, impedendo la tail recursion.

def square(l: List[Int]): List[Int] = ???

Da List a ListOttenere una nuova lista di elementi trasformati.Ad es. trasforma ogni intero nel suo quadrato:

def square(l: List[Int]): List[Int] = { if (l.isEmpty) Nil else (l.head * l.head) :: square(l.tail) }

def neg(l: List[Int]): List[Int] = if (l.isEmpty) Nil else (-l.head) :: neg(l.tail)

Da List a ListOttenere una nuova lista di elementi trasformati.Ad es. trasforma ogni intero nel suo quadrato:

Trasforma ogni elemento nel suo negato:

def transform(l: List[Int], f: (Int => Int)): List[Int] = ???

Trasformazione di una ListGeneralizziamo: data una generica funzione f() che trasforma interi in interi…

def transform(l: List[Int], f: (Int => Int)): List[Int] = { if (l.isEmpty) Nil else f(l.head) :: transform(l.tail, f)}

def negate(x: Int) = -xdef sqr(x: Int) = x * x

transform(a, negate)transform(a, sqr)

Trasformazione di una ListGeneralizziamo: data una generica funzione f() che trasforma interi in interi…

Trasformare una collezione di oggetti è un’operazione molto comune, tanto che List fornisce una funzione ad hoc per questo.

def map[B](f: (A) => B): List[B]

Builds a new collection by applying a function to all elements of this list.

List mappingDefinizione di map() per una List[A] (Scala API docs):

Il risultato è una nuova List di valori di tipo B, ottenuta applicando f a ogni elemento di tipo A della List di partenza.

Questa operazione si definisce trasformazione, o proiezione, o mapping.

a.map(negate)

b.map( (x: Int) => x % 2 )

b.map( (x: Int) => x % 2 == 0)

val bs = b.map( (x: Int) => if (x % 2 == 0) x + " e' pari" else x + " e' dispari" )

bs.mkString("\n")

List mappingAlcune operazioni di mapping, dove a e b sono una List[Int]:

Collection mapping in Java 8Anche Java ha il mapping delle collezioni:

Scala è davvero un linguaggio così complesso?

public class Employee { private String name; private Integer age; private Double salary;

// getters and setters}

List<String> employeeNames = employeeList .stream() .collect(Collectors.mapping(Employee::getName, Collectors.toList()));

Altre funzioni di ListOttenere una lista di elementi che soddisfano una condizione:

Verificare se una condizione vale per tutti gli elementi della lista:

b.filter( (x: Int) => x % 2 == 0)

b.forall( (x: Int) => x < 100 && x > 0)

Folding, that is: reduceOltre al mapping, l'altra operazione sui dati è reduce, o folding.

Sono le operazioni che, data una lista, riducono a un solo valore, in funzione di tutti i valori della lista. Un esempio banale: la somma di tutti gli interi.val sum = (x1: Int, x2: Int) => x1 + x2val l = List(1,5,7)l.foldLeft(0)(sum)

Folding, that is: reduceOltre al mapping, l'altra operazione sui dati è reduce, o folding.

Sono le operazioni che, data una lista, riducono a un solo valore, in funzione di tutti i valori della lista. Un esempio banale: la somma di tutti gli interi.val sum = (x1: Int, x2: Int) => x1 + x2val l = List(1,5,7)l.foldLeft(0)(sum)

0 + 1 = 1

Folding, that is: reduceOltre al mapping, l'altra operazione sui dati è reduce, o folding.

Sono le operazioni che, data una lista, riducono a un solo valore, in funzione di tutti i valori della lista. Un esempio banale: la somma di tutti gli interi.val sum = (x1: Int, x2: Int) => x1 + x2val l = List(1,5,7)l.foldLeft(0)(sum)

0 + 1 = 1

1 + 5 = 6

Folding, that is: reduceOltre al mapping, l'altra operazione sui dati è reduce, o folding.

Sono le operazioni che, data una lista, riducono a un solo valore, in funzione di tutti i valori della lista. Un esempio banale: la somma di tutti gli interi.val sum = (x1: Int, x2: Int) => x1 + x2val l = List(1,5,7)l.foldLeft(0)(sum)

0 + 1 = 1

1 + 5 = 6

6 + 7 = 13

Folding APIScala API, foldLeft per List[A]:

def foldLeft[B](z: B)(op: (B, A) ⇒ B): B

Applies a binary operator to a start value and all elements of this sequence, going left to right.

Quindi il tipo B del risultato è quello dell’operatore. Ad esempio, la stessa stringa di interi:l.foldLeft("0")((s: String, i: Int) => s + " + " + i)

Fold RightScala API, foldRight per List[A]:

def foldRight[B](z: B)(op: (A, B) ⇒ B): B

Applies a binary operator to all elements of this list and a start value, going right to left.

Ad esempio, qual è il risultato dil.foldRight("0")((i: Int, s: String) => s + " + " + i)

Collection di PersoneQualche esempio con la più classica delle class:case class Persona(nome: String, cognome: String, eta: Int)... val pippo = Persona("Pippo", "Rossi", 25) val mario = Persona("Mario", "Rossi", 28) val giovanni = Persona("Giovanni", "Neri", 19) val marco = Persona("Marco", "Bianchi", 36) val roberto = Persona("Roberto", "Verdi", 41) val laura = Persona("Laura", "Bianchi", 20) val michele = Persona("Michele", "Rossi", 15)

val people = List[Persona](pippo, mario, giovanni, marco, roberto, laura, michele)

Collection di PersoneCome si ottiene la lista dei nomi completi (nome e cognome)?

Come si trovano tutte le persone sotto i 24 anni?

Qual è l’età media?

Come si trovano tutti i parenti, cioè le persone con stesso cognome?

Eccezioni & PF

Eccezioni: perché no?All’inizio abbiamo elencato le eccezioni tra i side effect, incompatibili con la PF. Infatti le eccezioni non sono Referentially Transparent.

def MyFunc(i: Int): Int = { val x = 42 / i try { x } catch { case e: Exception => 55 }}

Se invoco MyFunc(0) ottengo un’eccezione.

Se sostituisco 42/i a x, ottengo 55!

Eccezioni: perché no?RT significa ragionare localmente, non in dipendenza dal contesto. Nell’esempio 42/i cambia elaborazione in base alla sua posizione, quindi dipendentemente dal contesto.

Inoltre il catch delle eccezioni rappresenta un salto incondizionato => codice difficile da comprendere e manutenere.

Da qui la regola generale:

usate le eccezioni per la gestione degli errori, non per il controllo del flusso.

Ma è solo un buon consiglio!

Eccezioni: anche noLe eccezioni non sono type safe. La funzione che abbiamo scritto:

def MyFunc(i: Int): Int

non dice nulla di possibili eccezioni. Se queste vengono “lanciate”, nessuno garantisce che il chiamante sia in grado di gestirle.

Eccezioni: anche noLe eccezioni non sono type safe. La funzione che abbiamo scritto:

def MyFunc(i: Int): Int

non dice nulla di possibili eccezioni. Se queste vengono “lanciate”, nessuno garantisce che il chiamante sia in grado di gestirle.

E le checked exception di Java?

In Java una funzione dichiara quali eccezioni può lanciare, e il caller deve gestirle (o rilanciarle).

Ma è davvero così utile?

Catchin’ exceptions: la disfatta!In realtà, alcuni ricercatori(1) hanno analizzato, tramite automatismo sw, progetti Java su GitHub e SourceForge.

Hanno così raccolto varie statistiche sul contenuto dei catch block.

(1) Analysis of Exception Handling Patterns in Java Projects: An Empirical Study - Suman Nakshatri, Maithri Hegde, Sahithi Thandra

Eccezioni nella PFLe checked exceptions non sono applicabili alle high-order functions.

Ad esempio list.map(f) ha come argomento una funzione f().

Che eccezioni deve gestire map()?

Come può gestire ogni possibile eccezione di ogni possibile f()?

In generale non è possibile. Occorre una gestione degli errori che sia compatibile con la metodologia funzionale.

Eccezioni nella PFdef mean(xs: List[Double]): Double = if (xs.isEmpty) throw new ArithmeticException("mean of empty list!") else xs.sum / xs.length

La funzione mean (media) è una partial function: non può essere calcolata per tutto il dominio di input.

Si potrebbe tornare un valore di guardia (c like): null, oppure 0.

Eccezioni nella PFdef mean(xs: List[Double]): Double = if (xs.isEmpty) null else xs.sum / xs.length

Problemi:

- non c’è controllo che il chiamante verifichi la guardia (per il compilatore, è trasparente)

- potrebbe non esserci un valore di guardia, ad esempio per i tipi primitivi

OptionIn Scala è definito il tipo Option[A] che ammette due soli valori:

● Some

● None

Some è un oggetto che contiene un valore di tipo A, che si può ottenere invocando get(). Con Option possiamo scrivere:

def mean(xs: List[Double]): Option[Double] =

if (xs.isEmpty) None

else Some(xs.sum / xs.length)

Optionval l = List(3d,5d,7d,11d)val m: Option[Double] = mean(l)m.get // torna 6.5

val n = mean(List())n.get // throws NoSuchElementException

if (n.isDefined) n.get else 0d // torna 0.0

// equivalente:n.getOrElse(0d)

OptionOption ha tutte le funzioni viste per List, ad esempio map():

def map[B](f: (A) ⇒ B): Option[B]

Returns a Some containing the result of applying f to this Option's value if this Option is nonempty. Otherwise return None.

La funzione f() viene applicata solo se Option è definito, quindi se contiene un valore di tipo A. Quindi questa funzione opera sempre sul valore contenuto in un Some, non dovendosi preoccupare di altro.

Grazie a map(), possiamo concentrarci “on the happy path!” (cit. Erik Meijer) e rimandare la gestione errori al momento più opportuno.

OptionTornando all’esempio della funzione mean(), dove m è Some(6.5) e n è None:

def toPercent(d: Double): String = d + "%"

m.map(toPercent)n.map(toPercent)

Tutt’e due le chiamate sono valide, e tornano...

Persone con OptionRiprendiamo la classe Persona, possiamo creare una funzione che cerca per nome nella lista people:

val people = List[Persona](pippo, mario, ...

def lookupByName(name: String): Option[Persona] = { val l = people.filter(p => p.nome == name) if (l.isEmpty) None else Some(l.head)}

Persone con OptionlookupByName("Paolo")lookupByName("Marco")

lookupByName("Paolo").map(_.eta).getOrElse(0)

lookupByName("Michele") .map(_.eta) .filter(_ > 18) .getOrElse(0)

Vediamo come gestire carte di credito che possono fallire

Uno stato funzionale

Generatore di numeri randomAnche Scala ha il "classico" generatore di numeri random:

val rng = new scala.util.Random

rng.nextInt

rng.nextInt(10)

nextInt() è una funzione pura?

Evidentemente no. Pensate a come testarla, ad esempio come testare:

val rng = new scala.util.Randomdef rollDie: Int = { rng.nextInt(6)}

Rendere le API con stato purePrendiamo una API che modifica uno stato interno (MyState):

class Foo { private var state: MyState = ... def bar: Bar def baz: Int}

Ipotesi: bar e baz sono funzioni che si basano su "state" e modificano (mutano) "state".

Come rendere tutto funzionale?

Rendere le API con stato pureStessa API, con stato "esplicito":

trait Foo { def bar: (Bar, Foo) def baz: (Int, Foo)}

bar e baz sono funzioni pure, la loro esecuzione è senza effetti collaterali.

Chi usa questa API è però responsabile di passare il giusto Foo nella chiamata.

Random Generator con stato funzionaletrait RNG { def nextInt: (Int, RNG)}

case class SimpleRNG(seed: Long) extends RNG { def nextInt: (Int, RNG) = { ... val nextRNG = SimpleRNG(newSeed) (n, nextRNG) }}

Random Generator con stato funzionaleEsempi di utilizzo della API:

def randomPair(rng: RNG): ((Int,Int), RNG) = { val (i1,rng2) = rng.nextInt val (i2,rng3) = rng2.nextInt ( (i1,i2), rng3 )}

def nonNegativeInt(rng: RNG): (Int, RNG) = { val (i, r) = rng.nextInt (if (i < 0) -(i + 1) else i, r)}

Nota: Int.Min = - Int.Max - 1 In questo modo -Int.Min diventa Int.Max

Function compositionQuindi dobbiamo sempre passare lo stato tra una chiamata e l'altra?

No, in generale le ripetizioni vengono "fattorizzate" tramite la composizione di funzioni.

type Rand[A] = RNG => (A, RNG)

def map[A,B](s: Rand[A])(f: A => B): Rand[B] = rng => { val (a, rng2) = s(rng) (f(a), rng2) }

map() esegue la transizione di stato, e applica una funzione al dato a.

Function compositionCon map() si può creare un generatore di numeri non negativi, e pari:

def nonNegativeEven: Rand[Int] = map(nonNegativeInt)(i => i - i % 2)

Magicamente non dobbiamo più occuparci di passare lo stato,

map() fa tutto il lavoro per noi.

ConclusioniIn definitiva, ogni programma dotato di uno stato può essere trasformato in un programma funzionale.

Il metodo diretto è creare sempre un nuovo, immutabile stato ad ogni transizione dello stesso.

Grazie alla composizione di funzioni (e ce ne sono tante, map è solo un esempio) è facile ottenere logiche complesse con poche righe di codice.

Bibliografia e web-ografia● Functional Programming in Scala - PAUL CHIUSANO,

RÚNAR BJARNASON - Manning● Scala By Example - Martin Odersky - EPFL● https://en.wikipedia.org/wiki/Functional_programming

I sorgenti usati sono disponibili qui:

https://github.com/midthegap/progfun

Recommended