Confuso com a compreensão da transformação flatMap / Map

87

Eu realmente não pareço estar entendendo Map e FlatMap. O que não consigo entender é como uma compreensão para é uma sequência de chamadas aninhadas para map e flatMap. O exemplo a seguir é de Functional Programming in Scala

def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = for {
            f <- mkMatcher(pat)
            g <- mkMatcher(pat2)
 } yield f(s) && g(s)

traduz para

def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = 
         mkMatcher(pat) flatMap (f => 
         mkMatcher(pat2) map (g => f(s) && g(s)))

O método mkMatcher é definido da seguinte maneira:

  def mkMatcher(pat:String):Option[String => Boolean] = 
             pattern(pat) map (p => (s:String) => p.matcher(s).matches)

E o método padrão é o seguinte:

import java.util.regex._

def pattern(s:String):Option[Pattern] = 
  try {
        Some(Pattern.compile(s))
   }catch{
       case e: PatternSyntaxException => None
   }

Seria ótimo se alguém pudesse lançar alguma luz sobre a lógica por trás do uso de map e flatMap aqui.

sc_ray
fonte

Respostas:

197

TL; DR vá diretamente para o exemplo final

Vou tentar recapitular.

Definições

A forcompreensão é um atalho de sintaxe para combinar flatMape mapde uma forma fácil de ler e raciocinar.

Vamos simplificar um pouco as coisas e assumir que todos os classque fornecem os dois métodos mencionados acima podem ser chamados de a monade usaremos o símbolo M[A]para significar a monadcom um tipo interno A.

Exemplos

Algumas mônadas comumente vistas incluem:

  • List[String] Onde
    • M[X] = List[X]
    • A = String
  • Option[Int] Onde
    • M[X] = Option[X]
    • A = Int
  • Future[String => Boolean] Onde
    • M[X] = Future[X]
    • A = (String => Boolean)

mapa e mapa plano

Definido em uma 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]

por exemplo

  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 expressão

  1. Cada linha na expressão que usa o <-símbolo é traduzida para uma flatMapchamada, exceto para a última linha que é traduzida para uma mapchamada final , onde o "símbolo vinculado" no lado esquerdo é passado como o parâmetro para a função do argumento (o que que chamamos 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. Uma for-expression com apenas um <-é convertida em uma mapchamada com a expressão passada 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

Agora ao ponto

Como você pode ver, a mapoperação preserva a "forma" do original monad, então o mesmo acontece para a yieldexpressão: a Listpermanece Listcom o conteúdo transformado pela operação no yield.

Por outro lado, cada linha de ligação no foré apenas uma composição de sucessivas monads, que devem ser "achatadas" para manter uma única "forma externa".

Suponha por um momento que cada ligação interna foi traduzida para uma mapchamada, mas a mão direita era a mesma A => M[B]função, você terminaria com um M[M[B]]para cada linha na compreensão.
A intenção de toda a forsintaxe é facilmente "nivelar" a concatenação de operações monádicas sucessivas (ou seja, operações que "levantam" um valor em uma "forma monádica":) A => M[B], com a adição de uma mapoperação final que possivelmente executa uma transformação conclusiva.

Espero que isso explique a lógica por trás da escolha da tradução, que é aplicada de forma mecânica, ou seja: n flatMapchamadas aninhadas concluídas por uma única mapchamada.

Um exemplo ilustrativo inventado
destinado a mostrar a expressividade da forsintaxe

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 (_ + _)
}

Você consegue adivinhar o tipo de valuesList?

Como já foi dito, a forma do monadé mantida através da compreensão, então começamos com um Listdentro company.branchese devemos terminar com um List.
Em vez disso, o tipo interno muda e é determinado pela yieldexpressão: que écustomer.value: Int

valueList devia ser um List[Int]

pagoda_5b
fonte
1
As palavras "é o mesmo que" pertencem à meta-linguagem e devem ser removidas do bloco de código.
dia
3
Todo iniciante em FP deve ler isso. Como isso pode ser alcançado?
mert inan
1
@melston Vamos dar um exemplo com Lists. Se você mapduas vezes uma função A => List[B](que é uma das operações monádicas essenciais) sobre algum valor, você termina com uma Lista [Lista [B]] (estamos assumindo que os tipos correspondem). O loop interno de compreensão de for compõe essas funções com a flatMapoperação correspondente , "achatando" a forma de Lista [Lista [B]] em uma Lista simples [B] ... Espero que esteja claro
pagoda_5b
1
foi simplesmente incrível ler sua resposta. Gostaria que você escrevesse um livro sobre scala, você tem um blog ou algo assim?
Tomer Ben David
1
@coolbreeze Pode ser que eu não tenha expressado isso claramente. O que eu quis dizer é que a yieldcláusula é customer.value, cujo tipo é Int, portanto, o todo for comprehensionavalia como a List[Int].
pagoda_5b
6

Não sou um scala mega mind, então sinta-se à vontade para me corrigir, mas é assim que eu explico a flatMap/map/for-comprehensionsaga para mim mesmo!

Para entender for comprehensione sua tradução scala's map / flatMap, devemos dar pequenos passos e entender as partes que o compõem - mape flatMap. Mas não é scala's flatMapmapcom flattenvocê se perguntar! em caso afirmativo, por que tantos desenvolvedores acham tão difícil entendê-lo ou dominá-lo for-comprehension / flatMap / map. Bem, se você olhar apenas para scala mape flatMapassinatura, verá que eles retornam o mesmo tipo de retorno M[B]e funcionam no mesmo argumento de entrada A(pelo menos a primeira parte da função que assumem), se é isso que faz a diferença?

Nosso plano

  1. Entenda o scala map.
  2. Entenda o scala flatMap.
  3. Entenda scala de for comprehension.`

Mapa de Scala

assinatura do mapa scala:

map[B](f: (A) => B): M[B]

Mas há uma grande parte faltando quando olhamos para essa assinatura, e é - de onde Avem isso ? nosso contêiner é do tipo, Aportanto, é importante examinar essa função no contexto do contêiner - M[A]. Nosso contêiner pode ser um Listde itens do tipo Ae nossa mapfunção tem uma função que transforma cada item do tipo Aem tipo B, então retorna um contêiner do tipo B(ou M[B])

Vamos escrever a assinatura do mapa levando em consideração o contêiner:

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]

Observe um fato extremamente importante sobre o mapa - ele se agrupa automaticamente no contêiner de saída que M[B]você não tem controle sobre ele. Vamos enfatizar novamente:

  1. mapescolhe o contêiner de saída para nós e ele será o mesmo contêiner da fonte em que trabalhamos, então, para o M[A]contêiner, obtemos o mesmo Mcontêiner apenas para B M[B]e nada mais!
  2. mapfaz essa conteinerização para nós nós apenas damos um mapeamento de Apara Be ele colocaria na caixa de M[B]irá colocá-lo na caixa para nós!

Você vê que não especificou como containerizeo item acabou de especificar como transformar os itens internos. E como temos o mesmo contêiner Mpara ambos M[A]e M[B]isso significa que M[B]é o mesmo contêiner, ou seja, se você tiver, List[A]então você terá um List[B]e, o mais importante, mapé fazer isso por você!

Agora que lidamos com isso, mapvamos prosseguir flatMap.

FlatMap de Scala

Vamos ver sua assinatura:

flatMap[B](f: (A) => M[B]): M[B] // we need to show it how to containerize the A into M[B]

Você vê a grande diferença de mapa para flatMapem flatMap; estamos fornecendo a função que não apenas converte, A to Bmas também o transforma em contêiner M[B].

por que nos importamos com quem faz a conteinerização?

Então, por que nos importamos tanto com a função de entrada para mapear / flatMap que faz a containerização M[B]ou o próprio mapa faz a containerização para nós?

Você vê, no contexto do for comprehensionque está acontecendo, várias transformações no item fornecido no, forportanto, estamos dando ao próximo trabalhador em nossa linha de montagem a capacidade de determinar a embalagem. imagine que temos uma linha de montagem, cada trabalhador faz alguma coisa com o produto e apenas o último trabalhador o está embalando em um contêiner! bem-vindo a flatMapeste é o seu propósito, em mapcada trabalhador, ao terminar de trabalhar no item, também o embala para que você coloque os recipientes sobre os recipientes.

O poderoso para a compreensão

Agora vamos dar uma olhada em sua compreensão levando em consideração o que dissemos acima:

def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = for {
    f <- mkMatcher(pat)   
    g <- mkMatcher(pat2)
} yield f(s) && g(s)

O que temos aqui:

  1. mkMatcherretorna um containero contêiner contém uma função:String => Boolean
  2. As regras são se tivermos vários para os <-quais elas se traduzem, flatMapexceto o último.
  3. Como f <- mkMatcher(pat)é o primeiro em sequence(pensar assembly line) tudo o que queremos é pegar fe passar para o próximo trabalhador na linha de montagem, deixamos o próximo trabalhador em nossa linha de montagem (a próxima função) a capacidade de determinar o que seria embalagem de volta do nosso item é por isso que a última função é map.
  4. O último g <- mkMatcher(pat2)usará mapisso porque é o último na linha de montagem! então ele pode apenas fazer a operação final com a map( g =>qual sim! retira ge usa o fque já foi retirado do contêiner pelo, flatMapportanto, acabamos com o primeiro:

    mkMatcher (pat) flatMap (f // puxar a função f dar o item ao próximo trabalhador da linha de montagem (você vê que ele tem acesso fe não empacota-o de volta, quero dizer, deixe o mapa determinar a embalagem, deixe o próximo trabalhador da linha de montagem determinar o container. mkMatcher (pat2) map (g => f (s) ...)) // como esta é a última função na linha de montagem, vamos usar map e puxar g para fora do contêiner e para a embalagem de volta , sua mape esta embalagem irão estrangular todo o caminho e ser nossa embalagem ou nosso contêiner, yah!

Tomer Ben David
fonte
4

A justificativa é encadear operações monádicas que fornecem como um benefício o tratamento adequado de erros de "falha rápida".

Na verdade, é muito simples. O mkMatchermétodo retorna um Option(que é uma Mônada). O resultado da mkMatcheroperação monádica é a Noneou a Some(x).

Aplicar a função mapou flatMapa Nonesempre retorna a None- a função passada como parâmetro mape flatMapnão é avaliada.

Portanto, em seu exemplo, se mkMatcher(pat)retornar um Nenhum, o flatMap aplicado a ele retornará a None(a segunda operação monádica mkMatcher(pat2)não será executada) e a final mapretornará novamente a None. Em outras palavras, se qualquer uma das operações na compreensão de for retornar um Nenhum, você terá um comportamento de falha rápida e o resto das operações não serão executadas.

Este é o estilo monádico de tratamento de erros. O estilo imperativo usa exceções, que são basicamente saltos (para uma cláusula catch)

Uma nota final: a patternsfunção é uma maneira típica de "traduzir" um tratamento de erros de estilo imperativo ( try... catch) para um tratamento de erros de estilo monádico usandoOption

Bruno Grieder
fonte
Você sabe por que flatMap(e não map) é usado para "concatenar" a primeira e a segunda invocação de mkMatcher, mas por que map(e não flatMap) é usado para "concatenar" a segunda mkMatchere o yieldsbloco?
Malte Schwerhoff
1
flatMapespera que você passe uma função retornando o resultado "empacotado" / levantado na Mônada, enquanto mapfará o empacotamento / levantamento propriamente dito. Durante o encadeamento de chamadas de operações no, for comprehensionvocê precisa para flatmapque as funções passadas como parâmetro possam retornar None(você não pode elevar o valor para Nenhum). yieldEspera-se que a última chamada de operação, aquela em, seja executada e retorne um valor; um mapencadeamento dessa última operação é suficiente e evita ter que elevar o resultado da função para a mônada.
Bruno Grieder
1

Isso pode ser traduzido 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)

Execute isto para uma melhor visão de como é expandido

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)

os resultados são:

1 -> a
1 -> b
1 -> c
...
2 -> a
2 -> b
...

Isso é semelhante a flatMap- fazer um loop através de cada elemento em pate foreach o elemento mappara cada elemento empat2

Korefn
fonte
0

Primeiro, mkMatcherretorna uma função cuja assinatura é String => Boolean, que é um procedimento java regular que acaba de ser executado Pattern.compile(string), conforme mostrado na patternfunção. Então, olhe para esta linha

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

A mapfunção é aplicada ao resultado de pattern, que é Option[Pattern], portanto, o pin p => xxxé apenas o padrão que você compilou. Assim, dado um padrão p, uma nova função é construída, que pega uma String se verifica se scorresponde ao padrão.

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

Observe que a pvariável está limitada ao padrão compilado. Agora, está claro como uma função com assinatura String => Booleané construída por mkMatcher.

A seguir, vamos verificar a bothMatchfunção em que se baseia mkMatcher. Para mostrar como bothMathchfunciona, primeiro olhamos para esta parte:

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

Visto que obtivemos uma função com assinatura String => Booleande mkMatcher, que está gneste contexto, g(s)é equivalente a Pattern.compile(pat2).macher(s).matches, que retorna se a String s corresponder ao padrão pat2. Então f(s), que tal, é o mesmo que g(s), a única diferença é que, a primeira chamada de mkMatcherusa flatMap, em vez de map, Por quê? Como mkMatcher(pat2) map (g => ....)retorna Option[Boolean], você obterá um resultado aninhado Option[Option[Boolean]]se usar mappara ambas as chamadas, não é isso que você deseja.

xiaowl
fonte