TL; DR ir directamente al ejemplo final
Intentaré recapitular.
Definiciones
La for
comprensión es un atajo de sintaxis para combinar flatMap
y map
de una manera que es fácil de leer y razonar.
Simplifiquemos un poco las cosas y supongamos que cada class
que proporciona los dos métodos antes mencionados se puede llamar a monad
y usaremos el símbolo M[A]
para significar a monad
con un tipo interno A
.
Ejemplos
Algunas mónadas que se ven comúnmente incluyen:
List[String]
dónde
M[X] = List[X]
A = String
Option[Int]
dónde
Future[String => Boolean]
dónde
M[X] = Future[X]
A = (String => Boolean)
mapa y plano
Definido en una mónada genérica M[A]
def map(f: A => B): M[B]
def flatMap(f: A => M[B]): M[B]
p.ej
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)
para expresarse
Cada línea de la expresión que usa el <-
símbolo se traduce a una flatMap
llamada, excepto la última línea que se traduce a una map
llamada final , donde el "símbolo enlazado" en el lado izquierdo se pasa como parámetro a la función del argumento (lo que llamamos anteriormente 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
Una expresión for con solo uno <-
se convierte en una map
llamada con la expresión pasada como argumento:
for {
bound <- list
} yield f(bound)
list.map { bound =>
f(bound)
}
list map f
Ahora al grano
Como puede ver, la map
operación conserva la "forma" del original monad
, por lo que lo mismo ocurre con la yield
expresión: a List
queda a List
con el contenido transformado por la operación en yield
.
Por otro lado, cada línea de encuadernación en el for
es solo una composición de sucesivas monads
, que deben ser "aplanadas" para mantener una única "forma externa".
Suponga por un momento que cada enlace interno se traduce en una map
llamada, pero la mano derecha tiene la misma A => M[B]
función, terminaría con una M[M[B]]
para cada línea de la comprensión.
La intención de toda la for
sintaxis es "aplanar" fácilmente la concatenación de operaciones monádicas sucesivas (es decir, operaciones que "levantan" un valor en una "forma monádica":) A => M[B]
, con la adición de una map
operación final que posiblemente realice una transformación final.
Espero que esto explique la lógica detrás de la elección de la traducción, que se aplica de forma mecánica, es decir: n
flatMap
llamadas anidadas concluidas por una sola map
llamada.
Un ejemplo ilustrativo elaborado con la
intención de mostrar la expresividad de la for
sintaxis
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 (_ + _)
}
¿Puedes adivinar el tipo de valuesList
?
Como ya se dijo, la forma del monad
se mantiene a través de la comprensión, por lo que comenzamos con un List
in company.branches
y debemos terminar con un List
.
En cambio, el tipo interno cambia y está determinado por la yield
expresión: que escustomer.value: Int
valueList
debería ser un List[Int]
Lists
. Simap
duplicas una funciónA => List[B]
(que es una de las operaciones monádicas esenciales) sobre algún valor, terminas con una Lista [Lista [B]] (damos por sentado que los tipos coinciden). El bucle interno para comprensión compone esas funciones con laflatMap
operación correspondiente , "aplanar" la forma de Lista [Lista [B]] en una Lista [B] simple ... Espero que esto quede claroyield
cláusula escustomer.value
, cuyo tipo esInt
, por lo tanto, el conjunto sefor comprehension
evalúa como aList[Int]
.No soy una mega mente de Scala, así que siéntete libre de corregirme, ¡pero así es como me explico la
flatMap/map/for-comprehension
saga!Para comprender
for comprehension
y su traducciónscala's map / flatMap
, debemos dar pequeños pasos y comprender las partes que componen,map
yflatMap
. Pero no esscala's flatMap
solomap
conflatten
le preguntas a ti mismo! Si es así, ¿por qué a tantos desarrolladores les resulta tan difícil entenderlo o comprenderlofor-comprehension / flatMap / map
? Bueno, si solo mira scalamap
y laflatMap
firma, verá que devuelven el mismo tipo de retornoM[B]
y funcionan con el mismo argumento de entradaA
(al menos la primera parte de la función que toman) si eso es así, ¿qué hace la diferencia?Nuestro plan
map
.flatMap
.for comprehension
.`Mapa de Scala
firma del mapa scala:
map[B](f: (A) => B): M[B]
Pero falta una gran parte cuando miramos esta firma, y es - ¿de dónde
A
viene esto ? nuestro contenedor es de tipo,A
por lo que es importante mirar esta función en el contexto del contenedor -M[A]
. Nuestro contenedor podría ser unList
elemento de tipoA
y nuestramap
función toma una función que transforma cada elemento de tipoA
en tipoB
, luego devuelve un contenedor de tipoB
(oM[B]
)Escribamos la firma del mapa teniendo en cuenta el contenedor:
M[A]: // We are in M[A] context. map[B](f: (A) => B): M[B] // map takes a function which knows to transform A to B and then it bundles them in M[B]
Tenga en cuenta un hecho extremadamente importante sobre el mapa : se agrupa automáticamente en el contenedor de salida,
M[B]
no tiene control sobre él. Destaquémoslo de nuevo:map
elige el contenedor de salida para nosotros y será el mismo contenedor que la fuente en la que trabajamos, por lo que para elM[A]
contenedor obtenemos el mismoM
contenedor solo paraB
M[B]
y nada más.map
hace esta contenedorización para nosotros, simplemente le damos un mapeo deA
aB
y lo pondría en el cuadro de ¡M[B]
lo pondrá en el cuadro por nosotros!Verá que no especificó cómo
containerize
el elemento que acaba de especificar cómo transformar los elementos internos. Y como tenemos el mismo contenedorM
para ambosM[A]
yM[B]
esto significa queM[B]
es el mismo contenedor, lo que significa que si lo tieneList[A]
, tendrá unList[B]
y, lo que es más importante, ¡map
lo hará por usted!Ahora que hemos tratado
map
, pasemos aflatMap
.Mapa plano de Scala
Veamos su firma:
flatMap[B](f: (A) => M[B]): M[B] // we need to show it how to containerize the A into M[B]
Verá la gran diferencia de map a
flatMap
en flatMap, le proporcionamos la función que no solo convierte,A to B
sino que también lo contiene en contenedoresM[B]
.¿Por qué nos importa quién realiza la contenedorización?
Entonces, ¿por qué nos preocupamos tanto de la función de entrada para map / flatMap en la contenedorización
M[B]
o el mapa en sí hace la contenedorización por nosotros?Como ve, en el contexto de
for comprehension
lo que está sucediendo, hay múltiples transformaciones en el artículo proporcionado en el,for
por lo que le estamos dando al siguiente trabajador en nuestra línea de ensamblaje la capacidad de determinar el empaque. ¡Imagínese que tenemos una línea de ensamblaje cada trabajador hace algo con el producto y solo el último trabajador lo empaca en un contenedor! bienvenido aflatMap
este es su propósito, enmap
cada trabajador cuando termina de trabajar en el artículo también lo empaqueta para que usted tenga contenedores sobre contenedores.El poderoso para la comprensión
Ahora analicemos su comprensión teniendo en cuenta lo que dijimos anteriormente:
def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = for { f <- mkMatcher(pat) g <- mkMatcher(pat2) } yield f(s) && g(s)
Qué tenemos aquí:
mkMatcher
devuelve uncontainer
el contenedor contiene una función:String => Boolean
<-
que se traducen,flatMap
excepto el último.f <- mkMatcher(pat)
es el primero ensequence
(pensarassembly line
) todo lo que queremos es tomarlof
y pasarlo al siguiente trabajador en la línea de montaje, dejamos que el siguiente trabajador de nuestra línea de montaje (la siguiente función) tenga la capacidad de determinar cuál sería el embalaje de nuestro artículo por eso la última función esmap
.¡El último
g <- mkMatcher(pat2)
usarámap
esto porque es el último en la línea de montaje! para que pueda hacer la operación final con lomap( g =>
que sí! sacag
y usa elf
que ya ha sido sacado del contenedor por el,flatMap
por lo tanto, terminamos con el primero:mkMatcher (pat) flatMap (f // extraer la función f entregar el artículo al siguiente trabajador de la línea de ensamblaje (ve que tiene acceso
f
y no empaquetarlo de nuevo, es decir, dejar que el mapa determine el empaque dejar que el siguiente trabajador de la línea de ensamblaje determine container. mkMatcher (pat2) map (g => f (s) ...)) // como esta es la última función en la línea de ensamblaje, usaremos map y sacaremos g del contenedor y regresaremos al empaque , sumap
y este empaque se acelerará por completo y será nuestro paquete o nuestro contenedor, ¡yah!fuente
La razón es encadenar operaciones monádicas, lo que proporciona como beneficio un manejo adecuado de errores "rápido de fallas".
En realidad, es bastante simple. El
mkMatcher
método devuelve unOption
(que es una mónada). El resultado demkMatcher
la operación monádica es aNone
o aSome(x)
.La aplicación de la función
map
oflatMap
a aNone
siempre devuelve aNone
: la función pasada como parámetromap
yflatMap
no se evalúa.Por lo tanto, en su ejemplo, si
mkMatcher(pat)
devuelve None, el flatMap que se le aplica devolverá aNone
(la segunda operación monádicamkMatcher(pat2)
no se ejecutará) y la finalmap
volverá a devolver aNone
. En otras palabras, si alguna de las operaciones en la comprensión, devuelve Ninguno, tiene un comportamiento de falla rápida y el resto de las operaciones no se ejecutan.Este es el estilo monádico de manejo de errores. El estilo imperativo usa excepciones, que son básicamente saltos (a una cláusula de captura)
Una nota final: la
patterns
función es una forma típica de "traducir" un manejo de errores de estilo imperativo (try
...catch
) a un manejo de errores de estilo monádico usandoOption
fuente
flatMap
(y nomap
) se usa para "concatenar" la primera y la segunda invocación demkMatcher
, pero por quémap
(y noflatMap
) se usa "concatenar" la segundamkMatcher
y elyields
bloque?flatMap
espera que pase una función que devuelva el resultado "envuelto" / levantado en la Mónada, mientrasmap
que él mismo hará el envasado / levantamiento. Durante el encadenamiento de llamadas de operaciones en elfor comprehension
, debe hacerloflatmap
para que las funciones pasadas como parámetro puedan regresarNone
(no puede elevar el valor a Ninguno). Seyield
espera que la última llamada a la operación, la que está en el, se ejecute y devuelva un valor; amap
para encadenar esa última operación es suficiente y evita tener que levantar el resultado de la función en la mónada.Esto se puede traducir como:
def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = for { f <- mkMatcher(pat) // for every element from this [list, array,tuple] g <- mkMatcher(pat2) // iterate through every iteration of pat } yield f(s) && g(s)
Ejecute esto para ver mejor cómo se expandió
def match items(pat:List[Int] ,pat2:List[Char]):Unit = for { f <- pat g <- pat2 } println(f +"->"+g) bothMatch( (1 to 9).toList, ('a' to 'i').toList)
los resultados son:
1 -> a 1 -> b 1 -> c ... 2 -> a 2 -> b ...
Esto es similar a
flatMap
- recorrer cada elemento enpat
ymap
para cada elemento a cada elemento enpat2
fuente
Primero,
mkMatcher
devuelve una función cuya firma esString => Boolean
, que es un procedimiento java normal que acaba de ejecutarsePattern.compile(string)
, como se muestra en lapattern
función. Entonces, mira esta líneapattern(pat) map (p => (s:String) => p.matcher(s).matches)
La
map
función se aplica al resultado depattern
, que esOption[Pattern]
, por lo quep
inp => xxx
es solo el patrón que compiló. Entonces, dado un patrónp
, se construye una nueva función, que toma una Cadenas
y verifica sis
coincide con el patrón.(s: String) => p.matcher(s).matches
Tenga en cuenta que la
p
variable está limitada al patrón compilado. Ahora, está claro que cómoString => Boolean
se construye una función con firmamkMatcher
.A continuación, revisemos la
bothMatch
función, que se basa enmkMatcher
. Para mostrar cómobothMathch
funciona, primero miramos esta parte:Dado que obtuvimos una función con firma
String => Boolean
demkMatcher
, que estág
en este contexto,g(s)
es equivalente aPattern.compile(pat2).macher(s).matches
, que devuelve si el String s coincide con el patrónpat2
. Entonces, ¿qué talf(s)
, es lo mismo queg(s)
, la única diferencia es que, la primera llamada demkMatcher
usosflatMap
, en lugar demap
, por qué? Debido a quemkMatcher(pat2) map (g => ....)
devuelveOption[Boolean]
, obtendrá un resultado anidadoOption[Option[Boolean]]
si lo usamap
para ambas llamadas, eso no es lo que desea.fuente