monad - for yield scala
Confundido con la comprensión forzosa de la transformación flatMap/Map (5)
Esto puede ser traducido 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 una mejor vista de 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
- flatMap
través de cada elemento en pat
y el elemento foreach pat2
a cada elemento en pat2
Realmente no entiendo Map y FlatMap. Lo que no estoy logrando entender es cómo una comprensión forzada es una secuencia de llamadas anidadas a map y flatMap. El siguiente ejemplo es de programación funcional en Scala
def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = for {
f <- mkMatcher(pat)
g <- mkMatcher(pat2)
} yield f(s) && g(s)
se traduce a
def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] =
mkMatcher(pat) flatMap (f =>
mkMatcher(pat2) map (g => f(s) && g(s)))
El método mkMatcher se define de la siguiente manera:
def mkMatcher(pat:String):Option[String => Boolean] =
pattern(pat) map (p => (s:String) => p.matcher(s).matches)
Y el método de patrón es el siguiente:
import java.util.regex._
def pattern(s:String):Option[Pattern] =
try {
Some(Pattern.compile(s))
}catch{
case e: PatternSyntaxException => None
}
Sería genial si alguien pudiera arrojar algo de luz sobre la razón detrás de usar map y flatMap aquí.
La razón fundamental es encadenar las operaciones monádicas, lo que proporciona como beneficio un correcto manejo de errores "fall-fast".
En realidad es bastante simple. El método mkMatcher
devuelve una Option
(que es una mónada). El resultado de mkMatcher
, la operación monádica, es None
o Some(x)
.
Aplicar el map
o la función flatMap
a una None
siempre devuelve un None
; la función pasada como parámetro para map
y flatMap
no se evalúa.
Por lo tanto, en su ejemplo, si mkMatcher(pat)
devuelve un None, el flatMap aplicado devolverá un None
(la segunda operación monádica mkMatcher(pat2)
no se ejecutará) y el map
final devolverá nuevamente un None
. En otras palabras, si cualquiera de las operaciones en la sección para comprensión, devuelve una 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 catch)
Una nota final: la función de patterns
es una forma típica de "traducir" un manejo de error de estilo imperativo ( try
... catch
) a un manejo de error de estilo monádico usando la Option
No soy una mega mente scala así que siéntete libre de corregirme, ¡pero así es como explico la flatMap/map/for-comprehension
para mí!
Para comprender la for comprehension
y su traducción al scala''s map / flatMap
, debemos tomar pequeños pasos y comprender las partes que componen: map
y flatMap
. Pero no es scala''s flatMap
map
scala''s flatMap
solo un map
scala''s flatMap
¡pregúntate a ti mismo! si es así, ¿por qué a tantos desarrolladores les resulta tan difícil comprenderlo o de for-comprehension / flatMap / map
? Bueno, si miras el map
de scala y la firma de flatMap
ves que devuelven el mismo tipo de retorno M[B]
y trabajan en el mismo argumento de entrada A
(al menos la primera parte de la función que toman) si eso es lo que hace ¿una diferencia?
Nuestro plan
- Comprenda el
map
de Scala. - Comprenda el
flatMap
de scala. - Comprender los scala
for comprehension
.
El mapa de Scala
firma de mapa scala:
map[B](f: (A) => B): M[B]
Pero falta una gran parte cuando vemos esta firma, y es de dónde viene esta A
? nuestro contenedor es de tipo A
por lo que es importante tener en cuenta esta función en el contexto del contenedor: M[A]
. Nuestro contenedor podría ser una List
de elementos de tipo A
y nuestra función de map
toma una función que transforma cada elemento de tipo A
en tipo B
, luego devuelve un contenedor de tipo B
(o M[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 sumamente importante sobre el mapa : se agrupa automáticamente en el contenedor de salida M[B]
no tiene control sobre él. Vamos a enfatizarlo de nuevo:
-
map
elige el contenedor de salida para nosotros y va a ser el mismo contenedor que la fuente en la que trabajamos, por lo que para el contenedorM[A]
obtenemos el mismo contenedorM
solo paraB
M[B]
y nada más. -
map
hace esta contenedorización para nosotros, solo damos un mapeo deA
aB
y lo ponemos en la caja deM[B]
lo pondremos en la caja para nosotros!
Verá que no especificó cómo containerize
el artículo que acaba de especificar cómo transformar los elementos internos. Y como tenemos el mismo contenedor M
tanto para M[A]
como para M[B]
esto significa que M[B]
es el mismo contenedor, lo que significa que si tiene List[A]
entonces tendrá una List[B]
y más importante aún, ¡el map
está haciendo por ti!
Ahora que hemos tratado el map
, pasemos a flatMap
.
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 del mapa a flatMap
en flatMap, le proporcionamos la función que no solo convierte de A to B
sino que también la contiene en M[B]
.
¿Por qué nos importa quién hace la contenedorización?
Entonces, ¿por qué nos preocupamos tanto de la función de entrada para mapear / flatMap si la contenedorización en M[B]
o el mapa mismo hace la contenedorización para nosotros?
En el contexto de la for comprehension
lo que está sucediendo son múltiples transformaciones en el artículo provisto, for
lo que le estamos dando al próximo trabajador de nuestra línea de ensamblaje la capacidad de determinar el empaque. ¡imagínese que tenemos una cadena de montaje, cada trabajador hace algo con el producto y solo el último trabajador lo está empaquetando en un contenedor! bienvenido a flatMap
este es su propósito, en el map
cada trabajador cuando termina de trabajar en el artículo también lo empaqueta para que pueda obtener 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
- Las reglas son las que si tenemos múltiples
<-
traducen aflatMap
excepto el último. - Como
f <- mkMatcher(pat)
es el primero en lasequence
(piense en laassembly line
), lo único que queremos es tomarf
y pasarlo al siguiente trabajador en la línea de ensamblaje, dejamos que el siguiente trabajador en nuestra línea de ensamblaje (el siguiente función) la capacidad de determinar cuál sería el empaque de nuestro artículo, por eso la última función es elmap
. ¡El último
g <- mkMatcher(pat2)
usará elmap
esto es porque es el último en la línea de ensamblaje! entonces puede hacer la operación final con elmap( g =>
que sí! sacag
y usa laf
que ya ha sido extraída del contenedor porflatMap
por lo tanto, terminamos primero:mkMatcher (pat) flatMap (f // saca la función f da el elemento al siguiente trabajador de la línea de ensamblaje (ves que tiene acceso a
f
, y no lo empaqueta, quiero decir, deja que el mapa determine el empaque, deja que el siguiente trabajador de la línea de montaje determine el contenedor. mkMatcher (pat2) map (g => f (s) ...)) // como esta es la última función en la línea de ensamblaje vamos a usar map y pull g del contenedor y al empaque ¡De vuelta, sumap
y este embalaje acelerarán hasta el final y serán nuestro paquete o nuestro contenedor, yah!
Primero, mkMatcher
devuelve una función cuya firma es String => Boolean
, que es un procedimiento java normal que simplemente ejecuta Pattern.compile(string)
, como se muestra en la función de pattern
. Entonces, mira esta línea
pattern(pat) map (p => (s:String) => p.matcher(s).matches)
La función de map
se aplica al resultado del pattern
, que es la Option[Pattern]
, por lo que p
en p => xxx
es solo el patrón que compiló. Entonces, dado un patrón p
, se construye una nueva función, que toma un String s
, y verifica si s
coincide con el patrón.
(s: String) => p.matcher(s).matches
Tenga en cuenta que la variable p
está limitada al patrón compilado. Ahora, está claro que mkMatcher
construye una función con la firma String => Boolean
.
A continuación, bothMatch
función bothMatch
, que se basa en mkMatcher
. Para mostrar cómo funciona bothMathch
, primero vemos esta parte:
mkMatcher(pat2) map (g => f(s) && g(s))
Como obtuvimos una función con la firma String => Boolean
de mkMatcher
, que es g
en este contexto, g(s)
es equivalente a Pattern.compile(pat2).macher(s).matches
, que devuelve si el String s coincide con el patrón pat2
. Entonces, ¿qué hay de f(s)
, es lo mismo que g(s)
, la única diferencia es que, la primera llamada de mkMatcher
utiliza flatMap
, en lugar de map
, ¿Por qué? Como el mkMatcher(pat2) map (g => ....)
devuelve la Option[Boolean]
, obtendrá un resultado anidado Option[Option[Boolean]]
si usa el map
para ambas llamadas, eso no es lo que desea.
TL; DR ir directamente al ejemplo final
Voy a tratar de recapitular
Definiciones
El 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 mencionados anteriormente se puede llamar monad
y usaremos el símbolo M[A]
para referirnos a una monad
con un tipo interno A
Ejemplos
Algunas mónadas comúnmente vistas
-
List[String]
donde-
M[_]: List[_]
-
A: String
-
-
Option[Int]
donde-
M[_]: Option[_]
-
A: Int
-
-
Future[String => Boolean]
donde-
M[_]: Future[_]
-
A: String => Boolean
-
mapa y planoMapa
Definido en una mónada genérica M[A]
/* applies a transformation of the monad "content" mantaining the
* monad "external shape"
* i.e. a List remains a List and an Option remains an Option
* but the inner type changes
*/
def map(f: A => B): M[B]
/* applies a transformation of the monad "content" by composing
* this monad with an operation resulting in another monad instance
* of the same type
*/
def flatMap(f: A => M[B]): M[B]
p.ej
val list = List("neo", "smith", "trinity")
//converts each character of the string to its corresponding code
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 la expresión
Cada línea en la expresión que usa el símbolo
<-
se traduce en una llamadaflatMap
, a excepción de la última línea que se traduce en una llamada almap
final, donde el "símbolo vinculado" en el lado izquierdo se pasa como el parámetro al función de argumento (lo que anteriormente llamamosf: A => M[B]
):// The following ... for { bound <- list out <- f(bound) } yield out // ... is translated by the Scala compiler as ... list.flatMap { bound => f(bound).map { out => out } } // ... which can be simplified as ... list.flatMap { bound => f(bound) } // ... which is just another way of writing: list flatMap f
Una expresión-for con solo un
<-
se convierte en una llamada demap
con la expresión pasada como argumento:// The following ... for { bound <- list } yield f(bound) // ... is translated by the Scala compiler as ... list.map { bound => f(bound) } // ... which is just another way of writing: list map f
Ahora al grano
Como puede ver, la operación de map
conserva la "forma" de la monad
original, por lo que sucede lo mismo con la expresión de yield
: una List
sigue siendo una List
con el contenido transformado por la operación en el yield
.
Por otro lado, cada línea de encuadernación en el for
es solo una composición de monads
sucesivas, que debe ser "aplanada" para mantener una única "forma externa".
Supongamos por un momento que cada enlace interno se tradujo en una llamada de map
, pero que la mano derecha era la misma función A => M[B]
, que terminaría con una M[M[B]]
por cada línea en el comprensión.
La intención del conjunto for
sintaxis es "aplanar" fácilmente la concatenación de sucesivas operaciones monádicas (es decir, operaciones que "elevan" un valor en una "forma monádica": A => M[B]
), con la adición de un final operación de map
que posiblemente realiza una transformación concluyente.
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
anidadas de flatMap
concluidas por una sola llamada de map
.
Un ejemplo ilustrativo artificial
Significó mostrar la expresividad de la 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)
valueList reduce (_ + _)
}
¿Puedes adivinar el tipo de valuesList
de valuesList
?
Como ya se dijo, la forma de la monad
se mantiene a través de la comprensión, por lo que comenzamos con una List
en la company.branches
, y debemos terminar con una List
.
El tipo interno en cambio cambia y está determinado por la expresión de yield
: que es customer.value: Int
valueList
debe ser una List[Int]