Confondu avec la transformation for-comprehension to flatMap / Map

87

Je ne semble vraiment pas comprendre Map et FlatMap. Ce que je n'arrive pas à comprendre, c'est en quoi une for-compréhension est une séquence d'appels imbriqués à map et flatMap. L'exemple suivant provient de la programmation fonctionnelle dans Scala

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

Se traduit par

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

La méthode mkMatcher est définie comme suit:

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

Et la méthode de modèle est la suivante:

import java.util.regex._

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

Ce serait formidable si quelqu'un pouvait faire la lumière sur la justification de l'utilisation de map et flatMap ici.

sc_ray
la source

Réponses:

197

TL; DR passe directement à l'exemple final

Je vais essayer de récapituler.

Définitions

La forcompréhension est un raccourci syntaxique à combiner flatMapet mapd'une manière facile à lire et à raisonner.

Simplifions un peu les choses et supposons que tout ce classqui fournit les deux méthodes susmentionnées peut être appelé a monadet nous utiliserons le symbole M[A]pour signifier a monadavec un type interne A.

Exemples

Certaines monades couramment observées comprennent:

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

map et flatMap

Défini dans une monade générique 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]

par exemple

  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)

pour l'expression

  1. Chaque ligne de l'expression utilisant le <-symbole est traduite en flatMapappel, à l'exception de la dernière ligne qui est traduite en mapappel final , où le "symbole lié" sur le côté gauche est passé comme paramètre à la fonction d'argument (quel nous avons précédemment appelé 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. Une expression for avec un seul <-est convertie en mapappel avec l'expression passée en argument:

    // 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

Maintenant au point

Comme vous pouvez le voir, l' mapopération préserve la "forme" de l'original monad, il en va de même pour l' yieldexpression: a Listreste a Listavec le contenu transformé par l'opération dans le yield.

D'autre part, chaque ligne de reliure dans le forest juste une composition successive monads, qui doit être "aplatie" pour conserver une seule "forme externe".

Supposons un instant que chaque liaison interne soit traduite en mapappel, mais que la main droite ait la même A => M[B]fonction, vous vous retrouveriez avec un M[M[B]]pour chaque ligne dans la compréhension.
L'intention de toute la forsyntaxe est de facilement "aplatir" la concaténation d'opérations monadiques successives (c'est-à-dire des opérations qui "soulèvent" une valeur dans une "forme monadique" :) A => M[B], avec l'ajout d'une mapopération finale qui effectue éventuellement une transformation finale.

J'espère que cela explique la logique du choix de la traduction, qui est appliquée de manière mécanique, c'est-à-dire: n flatMapdes appels imbriqués conclus par un seul mapappel.

Un exemple illustratif artificiel
destiné à montrer l'expressivité de la forsyntaxe

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

Pouvez-vous deviner le type de valuesList?

Comme déjà dit, la forme du monadest maintenue à travers la compréhension, donc nous commençons par un Listin company.branches, et devons finir par un List.
Le type interne change à la place et est déterminé par l' yieldexpression: qui estcustomer.value: Int

valueList devrait être un List[Int]

pagode_5b
la source
1
Les mots «est le même que» appartiennent au méta-langage et doivent être déplacés hors du bloc de code.
jour
3
Chaque débutant en FP devrait lire ceci. Comment cela peut il etre accompli?
mert inan
1
@melston Faisons un exemple avec Lists. Si vous mapdoublez une fonction A => List[B](qui est l'une des opérations monadiques essentielles) sur une valeur, vous vous retrouvez avec une List [List [B]] (nous prenons pour acquis que les types correspondent). La boucle interne pour la compréhension compose ces fonctions avec l' flatMapopération correspondante , "aplatir" la forme Liste [Liste [B]] en une simple Liste [B] ... J'espère que c'est clair
pagoda_5b
1
il a été purement génial de lire votre réponse. J'aimerais que vous écriviez un livre sur scala, avez-vous un blog ou quelque chose?
Tomer Ben David le
1
@coolbreeze Il se peut que je ne l'ai pas exprimé clairement. Ce que je voulais dire, c'est que la yieldclause est customer.value, dont le type est Int, donc l'ensemble for comprehensions'évalue à a List[Int].
pagoda_5b
6

Je ne suis pas un méga esprit scala alors n'hésitez pas à me corriger, mais c'est ainsi que je m'explique la flatMap/map/for-comprehensionsaga!

Pour comprendre for comprehensionet sa traduction, scala's map / flatMapnous devons faire de petits pas et comprendre les parties qui composent - mapet flatMap. Mais ce n'est pas scala's flatMapseulement mapavec flattenvous demandez-vous! si tel est le cas, pourquoi tant de développeurs ont-ils tant de mal à comprendre ou à comprendre for-comprehension / flatMap / map. Eh bien, si vous regardez simplement les scala mapet la flatMapsignature, vous voyez qu'ils renvoient le même type de retour M[B]et qu'ils fonctionnent sur le même argument d'entrée A(au moins la première partie de la fonction qu'ils prennent) si c'est le cas, qu'est-ce qui fait la différence?

Notre plan

  1. Comprenez les scala map.
  2. Comprenez les scala flatMap.
  3. Comprenez les scala. for comprehension»

Carte de Scala

signature de la carte scala:

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

Mais il manque une grande partie quand on regarde cette signature, et c'est - d'où cela Avient-il? notre conteneur est de type, Ail est donc important de regarder cette fonction dans le contexte du conteneur - M[A]. Notre conteneur pourrait être un Listélément de type Aet notre mapfonction prend une fonction qui transforme chaque élément de type Aen type B, puis elle retourne un conteneur de type B(ou M[B])

Écrivons la signature de la carte en tenant compte du conteneur:

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]

Notez un fait extrêmement important à propos de la carte : elle se regroupe automatiquement dans le conteneur de sortie, M[B]vous n'avez aucun contrôle dessus. Soulignons encore une fois:

  1. mapchoisit le conteneur de sortie pour nous et ce sera le même conteneur que la source sur laquelle nous travaillons, donc pour le M[A]conteneur, nous obtenons le même Mconteneur uniquement pour B M[B]et rien d'autre!
  2. mapfait cette conteneurisation pour nous, nous donnons juste un mappage de Aà Bet il le mettrait dans la boîte de M[B]le mettra dans la boîte pour nous!

Vous voyez que vous n'avez pas spécifié comment containerizel'élément que vous venez de spécifier comment transformer les éléments internes. Et comme nous avons le même conteneur Mpour les deux M[A]et que M[B]cela signifie que M[B]c'est le même conteneur, ce qui signifie que si vous en avez, List[A]vous allez en avoir un List[B]et, plus important encore, le mapfaire pour vous!

Maintenant que nous avons traité map, passons à flatMap.

Plan d'appartement de Scala

Voyons sa signature:

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

Vous voyez la grande différence entre map et flatMapflatMap, nous lui fournissons la fonction qui ne se contente pas de la convertir, A to Bmais également de la conteneuriser M[B].

pourquoi nous soucions-nous de qui fait la conteneurisation?

Alors pourquoi nous soucions-nous autant de la fonction d'entrée de map / flatMap la conteneurisation dans M[B]ou la carte elle-même fait la conteneurisation pour nous?

Vous voyez que dans le contexte de for comprehensionce qui se passe, il y a plusieurs transformations sur l'article fourni dans le for. Nous donnons donc au prochain travailleur de notre chaîne de montage la possibilité de déterminer l'emballage. Imaginez que nous ayons une chaîne de montage, chaque travailleur fait quelque chose sur le produit et que seul le dernier travailleur l'emballe dans un conteneur! bienvenue à flatMapceci est son but, dans mapchaque ouvrier une fois fini de travailler sur l'élément, le conditionne également afin que vous ayez des conteneurs sur les conteneurs.

Le puissant pour la compréhension

Examinons maintenant votre compréhension en tenant compte de ce que nous avons dit ci-dessus:

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

Qu'avons-nous ici:

  1. mkMatcherretourne a containerle conteneur contient une fonction:String => Boolean
  2. Les règles sont les si nous avons plusieurs, <-elles se traduisent à l' flatMapexception du dernier.
  3. Comme f <- mkMatcher(pat)c'est d'abord sequence(penser assembly line) tout ce que nous voulons en sortir est de le prendre fet de le transmettre au prochain ouvrier de la chaîne de montage, nous laissons au prochain ouvrier de notre chaîne de montage (la fonction suivante) la possibilité de déterminer ce que serait le emballage arrière de notre article c'est pourquoi la dernière fonction est map.
  4. Le dernier g <- mkMatcher(pat2)utilisera mapceci parce que son dernier en ligne d'assemblage! donc il peut simplement faire l'opération finale avec map( g =>laquelle oui! sort get utilise le fqui a déjà été retiré du conteneur par le flatMapdonc nous nous retrouvons avec en premier:

    mkMatcher (pat) flatMap (f // fonction d'extraction f donne un article au prochain ouvrier de la chaîne d'assemblage (vous voyez qu'il a accès à f, et ne le remballe pas, je veux dire, laissez la carte déterminer l'emballage, laissez le prochain ouvrier de la chaîne d'assemblage déterminer le container. mkMatcher (pat2) map (g => f (s) ...)) // comme il s'agit de la dernière fonction de la chaîne de montage, nous allons utiliser map et retirer g du conteneur et le remettre dans l'emballage , son mapet cet emballage ralentiront complètement et seront notre colis ou notre conteneur, yah!

Tomer Ben David
la source
4

La logique est d'enchaîner les opérations monadiques, ce qui offre comme avantage une gestion correcte des erreurs "échouez rapidement".

C'est en fait assez simple. La mkMatcherméthode renvoie un Option(qui est une Monade). Le résultat de mkMatcher, l'opération monadique, est soit a, Nonesoit a Some(x).

L'application de la fonction mapou flatMapà a Nonerenvoie toujours a None- la fonction passée en paramètre à mapet flatMapn'est pas évaluée.

Par conséquent, dans votre exemple, si mkMatcher(pat)renvoie un None, le flatMap qui lui est appliqué renverra a None(la deuxième opération monadique mkMatcher(pat2)ne sera pas exécutée) et la finale maprenverra à nouveau a None. En d'autres termes, si l'une des opérations de la compréhension for, renvoie un None, vous avez un comportement d'échec rapide et le reste des opérations ne sont pas exécutées.

C'est le style monadique de gestion des erreurs. Le style impératif utilise des exceptions, qui sont essentiellement des sauts (vers une clause catch)

Une dernière remarque: la patternsfonction est un moyen typique de "traduire" une gestion d'erreur de style impérative ( try... catch) en une gestion d'erreur de style monadique utilisantOption

Bruno Grieder
la source
Savez-vous pourquoi flatMap(et non map) est utilisé pour "concaténer" le premier et le second appel de mkMatcher, mais pourquoi map(et non flatMap) est utilisé "concaténer" le second mkMatcheret le yieldsbloc?
Malte Schwerhoff
1
flatMaps'attend à ce que vous passiez une fonction retournant le résultat «enveloppé» / soulevé dans la Monade, tandis que vous mapeffectuerez l'enrubannage / levage lui-même. Pendant le chaînage des appels des opérations dans le, for comprehensionvous devez le faire flatmappour que les fonctions passées en paramètre puissent être renvoyées None(vous ne pouvez pas élever la valeur en None). Le dernier appel d'opération, celui de la yieldest censé s'exécuter et renvoyer une valeur; a mappour enchaîner cette dernière opération suffit et évite d'avoir à soulever le résultat de la fonction dans la monade.
Bruno Grieder
1

Cela peut être traduit comme:

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)

Exécutez ceci pour une meilleure vue de la façon dont il est développé

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)

les résultats sont:

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

Ceci est similaire à flatMap- boucle à travers chaque élément dans patet mappour chaque élément à chaque élément danspat2

korefn
la source
0

Tout d'abord, mkMatcherretourne une fonction dont la signature est String => Boolean, c'est une procédure Java régulière qui vient de s'exécuter Pattern.compile(string), comme indiqué dans la patternfonction. Ensuite, regardez cette ligne

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

La mapfonction est appliquée au résultat de pattern, ce qui Option[Pattern]signifie que le pin p => xxxest simplement le modèle que vous avez compilé. Ainsi, étant donné un modèle p, une nouvelle fonction est construite, qui prend une chaîne set vérifie si elle scorrespond au modèle.

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

Notez que la pvariable est liée au modèle compilé. Maintenant, il est clair que la façon dont une fonction avec signature String => Booleanest construite par mkMatcher.

Ensuite, vérifions la bothMatchfonction, qui est basée sur mkMatcher. Pour montrer comment bothMathchfonctionne, nous regardons d'abord cette partie:

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

Puisque nous avons une fonction avec la signature String => Booleande mkMatcher, qui est gdans ce contexte, g(s)équivaut à Pattern.compile(pat2).macher(s).matches, qui renvoie si la chaîne correspond au modèle pat2. Alors que diriez-vous f(s), c'est la même chose que g(s), la seule différence est que, le premier appel aux mkMatcherutilisations flatMap, au lieu de map, pourquoi? Parce que mkMatcher(pat2) map (g => ....)renvoie Option[Boolean], vous obtiendrez un résultat imbriqué Option[Option[Boolean]]si vous utilisez mappour les deux appels, ce n'est pas ce que vous voulez.

xiaowl
la source