what monad for scala map monads for-comprehension

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

  1. Comprenda el map de Scala.
  2. Comprenda el flatMap de scala.
  3. 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:

  1. 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 contenedor M[A] obtenemos el mismo contenedor M solo para B M[B] y nada más.
  2. map hace esta contenedorización para nosotros, solo damos un mapeo de A a B y lo ponemos en la caja de M[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í:

  1. mkMatcher devuelve un container el contenedor contiene una función: String => Boolean
  2. Las reglas son las que si tenemos múltiples <- traducen a flatMap excepto el último.
  3. Como f <- mkMatcher(pat) es el primero en la sequence (piense en la assembly line ), lo único que queremos es tomar f 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 el map .
  4. ¡El último g <- mkMatcher(pat2) usará el map esto es porque es el último en la línea de ensamblaje! entonces puede hacer la operación final con el map( g => que sí! saca g y usa la f que ya ha sido extraída del contenedor por flatMap 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, su map 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

  1. Cada línea en la expresión que usa el símbolo <- se traduce en una llamada flatMap , a excepción de la última línea que se traduce en una llamada al map final, donde el "símbolo vinculado" en el lado izquierdo se pasa como el parámetro al función de argumento (lo que anteriormente llamamos f: 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

  2. Una expresión-for con solo un <- se convierte en una llamada de map 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]