TL; DR vai direttamente all'esempio finale
Proverò a ricapitolare.
Definizioni
La forcomprensione è una scorciatoia di sintassi da combinare flatMape mapin un modo che è facile da leggere e da ragionare.
Semplifichiamo un po 'le cose e supponiamo che ogni classmetodo che fornisce entrambi i metodi sopra menzionati possa essere chiamato a monade useremo il simbolo M[A]per indicare a monadcon 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 flatMapchiamata, ad eccezione dell'ultima riga che viene tradotta in una mapchiamata 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 mapchiamata 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' mapoperazione preserva la "forma" dell'originale monad, quindi lo stesso accade per l' yieldespressione: a Listrimane a Listcon 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 mapchiamata, 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 forsintassi è 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 mapche 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 flatMapchiamate annidate concluse da una singola mapchiamata.
Un esempio illustrativo artificioso
inteso a mostrare l'espressività della forsintassi
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 monadviene mantenuta attraverso la comprensione, quindi iniziamo con a Listin company.branchese dobbiamo terminare con a List.
Il tipo interno invece cambia ed è determinato yielddall'espressione: che ècustomer.value: Int
valueList dovrebbe essere un file List[Int]