TL; DR vai direttamente all'esempio finale
Proverò a ricapitolare.
Definizioni
La for
comprensione è una scorciatoia di sintassi da combinare flatMap
e map
in un modo che è facile da leggere e da ragionare.
Semplifichiamo un po 'le cose e supponiamo che ogni class
metodo che fornisce entrambi i metodi sopra menzionati possa essere chiamato a monad
e useremo il simbolo M[A]
per indicare a monad
con un tipo interno A
.
Esempi
Alcune monadi comunemente viste includono:
List[String]
dove
M[X] = List[X]
A = String
Option[Int]
dove
Future[String => Boolean]
dove
M[X] = Future[X]
A = (String => Boolean)
mappa e flatMap
Definito in una monade generica M[A]
def map(f: A => B): M[B]
def flatMap(f: A => M[B]): M[B]
per esempio
val list = List("neo", "smith", "trinity")
val f: String => List[Int] = s => s.map(_.toInt).toList
list map f
>> List(List(110, 101, 111), List(115, 109, 105, 116, 104), List(116, 114, 105, 110, 105, 116, 121))
list flatMap f
>> List(110, 101, 111, 115, 109, 105, 116, 104, 116, 114, 105, 110, 105, 116, 121)
per espressione
Ogni riga dell'espressione che utilizza il <-
simbolo viene tradotta in una flatMap
chiamata, ad eccezione dell'ultima riga che viene tradotta in una map
chiamata conclusiva , dove il "simbolo vincolato" sul lato sinistro viene passato come parametro alla funzione argomento (cosa abbiamo chiamato in precedenza f: A => M[B]
):
for {
bound <- list
out <- f(bound)
} yield out
list.flatMap { bound =>
f(bound).map { out =>
out
}
}
list.flatMap { bound =>
f(bound)
}
list flatMap f
Un'espressione for con una sola <-
viene convertita in una map
chiamata con l'espressione passata come argomento:
for {
bound <- list
} yield f(bound)
list.map { bound =>
f(bound)
}
list map f
Adesso al punto
Come puoi vedere, l' map
operazione preserva la "forma" dell'originale monad
, quindi lo stesso accade per l' yield
espressione: a List
rimane a List
con il contenuto trasformato dall'operazione in yield
.
D'altra parte ogni linea di rilegatura in for
è solo una composizione di successive monads
, che devono essere "appiattite" per mantenere un'unica "forma esterna".
Supponiamo per un momento che ogni rilegatura interna sia stata tradotta in una map
chiamata, ma la mano destra fosse la stessa A => M[B]
funzione, finiresti con un M[M[B]]
per ogni riga nella comprensione.
L'intento dell'intera for
sintassi è di "appiattire" facilmente la concatenazione di operazioni monadiche successive (cioè operazioni che "elevano" un valore in una "forma monadica" :) A => M[B]
, con l'aggiunta di un'operazione finale map
che possibilmente esegue una trasformazione conclusiva.
Spero che questo spieghi la logica alla base della scelta della traduzione, che viene applicata in modo meccanico, ovvero: n
flatMap
chiamate annidate concluse da una singola map
chiamata.
Un esempio illustrativo artificioso
inteso a mostrare l'espressività della for
sintassi
case class Customer(value: Int)
case class Consultant(portfolio: List[Customer])
case class Branch(consultants: List[Consultant])
case class Company(branches: List[Branch])
def getCompanyValue(company: Company): Int = {
val valuesList = for {
branch <- company.branches
consultant <- branch.consultants
customer <- consultant.portfolio
} yield (customer.value)
valuesList reduce (_ + _)
}
Riuscite a indovinare il tipo di valuesList
?
Come già detto, la forma del monad
viene mantenuta attraverso la comprensione, quindi iniziamo con a List
in company.branches
e dobbiamo terminare con a List
.
Il tipo interno invece cambia ed è determinato yield
dall'espressione: che ècustomer.value: Int
valueList
dovrebbe essere un file List[Int]