Confundido con la comprensión de la transformación flatMap / Map

87

Realmente no parece entender Map y FlatMap. Lo que no entiendo es cómo una para la comprensión 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 en

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 del 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 de ser del uso de map y flatMap aquí.

sc_ray
fuente

Respuestas:

200

TL; DR ir directamente al ejemplo final

Intentaré recapitular.

Definiciones

La forcomprensión es un atajo de sintaxis para combinar flatMapy mapde una manera que es fácil de leer y razonar.

Simplifiquemos un poco las cosas y supongamos que cada classque proporciona los dos métodos antes mencionados se puede llamar a monady usaremos el símbolo M[A]para significar a monadcon 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
    • M[X] = Option[X]
    • A = Int
  • Future[String => Boolean] dónde
    • M[X] = Future[X]
    • A = (String => Boolean)

mapa y plano

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 expresarse

  1. Cada línea de la expresión que usa el <-símbolo se traduce a una flatMapllamada, excepto la última línea que se traduce a una mapllamada 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]):

    // 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 uno <-se convierte en una mapllamada 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 mapoperación conserva la "forma" del original monad, por lo que lo mismo ocurre con la yieldexpresión: a Listqueda a Listcon el contenido transformado por la operación en yield.

Por otro lado, cada línea de encuadernación en el fores 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 mapllamada, 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 forsintaxis 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 mapoperació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 flatMapllamadas anidadas concluidas por una sola mapllamada.

Un ejemplo ilustrativo elaborado con la
intención de mostrar la expresividad de la forsintaxis

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 monadse mantiene a través de la comprensión, por lo que comenzamos con un Listin company.branchesy debemos terminar con un List.
En cambio, el tipo interno cambia y está determinado por la yieldexpresión: que escustomer.value: Int

valueList debería ser un List[Int]

pagoda_5b
fuente
1
Las palabras "es lo mismo que" pertenecen al metalenguaje y deben sacarse del bloque de código.
día
3
Todo principiante de FP debería leer esto. ¿Cómo se puede lograr esto?
mert inan
1
@melston Hagamos un ejemplo con Lists. Si mapduplicas una función A => 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 la flatMapoperación correspondiente , "aplanar" la forma de Lista [Lista [B]] en una Lista [B] simple ... Espero que esto quede claro
pagoda_5b
1
Ha sido pura maravilla leer tu respuesta. Me gustaría que escribieras un libro sobre scala, ¿tienes un blog o algo?
Tomer Ben David
1
@coolbreeze Puede ser que no lo expresé claramente. Lo que quise decir es que la yieldcláusula es customer.value, cuyo tipo es Int, por lo tanto, el conjunto se for comprehensionevalúa como a List[Int].
pagoda_5b
7

No soy una mega mente de Scala, así que siéntete libre de corregirme, ¡pero así es como me explico la flatMap/map/for-comprehensionsaga!

Para comprender for comprehensiony su traducción scala's map / flatMap, debemos dar pequeños pasos y comprender las partes que componen, mapy flatMap. Pero no es scala's flatMapsolo mapcon flattenle preguntas a ti mismo! Si es así, ¿por qué a tantos desarrolladores les resulta tan difícil entenderlo o comprenderlo for-comprehension / flatMap / map? Bueno, si solo mira scala mapy la flatMapfirma, verá que devuelven el mismo tipo de retorno M[B]y funcionan con el mismo argumento de entrada A(al menos la primera parte de la función que toman) si eso es así, ¿qué hace la diferencia?

Nuestro plan

  1. Entiende scala's map.
  2. Entiende scala's flatMap.
  3. Entiende scala's 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 Aviene esto ? nuestro contenedor es de tipo, Apor lo que es importante mirar esta función en el contexto del contenedor - M[A]. Nuestro contenedor podría ser un Listelemento de tipo Ay nuestra mapfunción toma una función que transforma cada elemento de tipo Aen 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 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:

  1. mapelige el contenedor de salida para nosotros y será el mismo contenedor que la fuente en la que trabajamos, por lo que para el M[A]contenedor obtenemos el mismo Mcontenedor solo para B M[B]y nada más.
  2. maphace esta contenedorización para nosotros, simplemente le damos un mapeo de Aa By lo pondría en el cuadro de ¡ M[B]lo pondrá en el cuadro por nosotros!

Verá que no especificó cómo containerizeel elemento que acaba de especificar cómo transformar los elementos internos. Y como tenemos el mismo contenedor Mpara ambos M[A]y M[B]esto significa que M[B]es el mismo contenedor, lo que significa que si lo tiene List[A], tendrá un List[B]y, lo que es más importante, ¡ maplo hará por usted!

Ahora que hemos tratado map, pasemos a flatMap.

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 flatMapen flatMap, le proporcionamos la función que no solo convierte, A to Bsino que también lo contiene en contenedores M[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 comprehensionlo que está sucediendo, hay múltiples transformaciones en el artículo proporcionado en el, forpor 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 a flatMapeste es su propósito, en mapcada 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í:

  1. mkMatcherdevuelve un containerel contenedor contiene una función:String => Boolean
  2. Las reglas son si tenemos varios a los <-que se traducen, flatMapexcepto el último.
  3. Como f <- mkMatcher(pat)es el primero en sequence(pensar assembly line) todo lo que queremos es tomarlo fy 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 es map.
  4. ¡El último g <- mkMatcher(pat2)usará mapesto porque es el último en la línea de montaje! para que pueda hacer la operación final con lo map( g =>que sí! saca gy usa el fque ya ha sido sacado del contenedor por el, flatMappor 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 fy 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 , su mapy este empaque se acelerará por completo y será nuestro paquete o nuestro contenedor, ¡yah!

Tomer Ben David
fuente
4

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 mkMatchermétodo devuelve un Option(que es una mónada). El resultado de mkMatcherla operación monádica es a Noneo a Some(x).

La aplicación de la función mapo flatMapa a Nonesiempre devuelve a None: la función pasada como parámetro mapy flatMapno se evalúa.

Por lo tanto, en su ejemplo, si mkMatcher(pat)devuelve None, el flatMap que se le aplica devolverá a None(la segunda operación monádica mkMatcher(pat2)no se ejecutará) y la final mapvolverá a devolver a None. 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 patternsfunció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

Bruno Grieder
fuente
¿Sabes por qué flatMap(y no map) se usa para "concatenar" la primera y la segunda invocación de mkMatcher, pero por qué map(y no flatMap) se usa "concatenar" la segunda mkMatchery el yieldsbloque?
Malte Schwerhoff
1
flatMapespera que pase una función que devuelva el resultado "envuelto" / levantado en la Mónada, mientras mapque él mismo hará el envasado / levantamiento. Durante el encadenamiento de llamadas de operaciones en el for comprehension, debe hacerlo flatmappara que las funciones pasadas como parámetro puedan regresar None(no puede elevar el valor a Ninguno). Se yieldespera que la última llamada a la operación, la que está en el, se ejecute y devuelva un valor; a mappara encadenar esa última operación es suficiente y evita tener que levantar el resultado de la función en la mónada.
Bruno Grieder
1

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 en paty mappara cada elemento a cada elemento enpat2

Corea
fuente
0

Primero, mkMatcherdevuelve una función cuya firma es String => Boolean, que es un procedimiento java normal que acaba de ejecutarse Pattern.compile(string), como se muestra en la patternfunción. Entonces, mira esta línea

pattern(pat) map (p => (s:String) => p.matcher(s).matches)

La mapfunción se aplica al resultado de pattern, que es Option[Pattern], por lo que pin p => xxxes solo el patrón que compiló. Entonces, dado un patrón p, se construye una nueva función, que toma una Cadena sy verifica si scoincide con el patrón.

(s: String) => p.matcher(s).matches

Tenga en cuenta que la pvariable está limitada al patrón compilado. Ahora, está claro que cómo String => Booleanse construye una función con firma mkMatcher.

A continuación, revisemos la bothMatchfunción, que se basa en mkMatcher. Para mostrar cómo bothMathchfunciona, primero miramos esta parte:

mkMatcher(pat2) map (g => f(s) && g(s))

Dado que obtuvimos una función con firma String => Booleande mkMatcher, que está gen 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é tal f(s), es lo mismo que g(s), la única diferencia es que, la primera llamada de mkMatcherusos flatMap, en lugar de map, por qué? Debido a que mkMatcher(pat2) map (g => ....)devuelve Option[Boolean], obtendrá un resultado anidado Option[Option[Boolean]]si lo usa mappara ambas llamadas, eso no es lo que desea.

xiaowl
fuente