web-dev-qa-db-fra.com

Confondu avec la for-comprehension à la transformation flatMap / Map

Je ne semble vraiment pas comprendre Map et FlatMap. Ce que je n'arrive pas à comprendre, c'est comment une for-comprehension est une séquence d'appels imbriqués pour mapper et flatMap. L'exemple suivant est tiré de 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 du 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 sera formidable si quelqu'un pouvait faire la lumière sur la raison derrière l'utilisation de map et flatMap ici.

81
sc_ray

TL; DR allez directement à l'exemple final

Je vais essayer de récapituler.

Définitions

La compréhension for est un raccourci de syntaxe pour combiner flatMap et map d'une manière facile à lire et à raisonner.

Simplifions un peu les choses et supposons que chaque class qui fournit les deux méthodes susmentionnées peut être appelé monad et nous utiliserons le symbole _M[A]_ pour désigner un monad avec un type interne A.

Exemples

Certaines monades courantes comprennent:

  • _List[String]_ où
    • _M[X] = List[X]_
    • _A = String_
  • _Option[Int]_ où
    • _M[X] = Option[X]_
    • _A = Int_
  • _Future[String => Boolean]_ où
    • _M[X] = Future[X]_
    • A = (String => Boolean)

carte 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 un appel flatMap, à l'exception de la dernière ligne qui est traduite en un appel de conclusion map, où le "symbole lié" sur le côté gauche est transmis comme paramètre de la fonction d'argument (ce que nous appelions précédemment _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 un appel map 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'opération map préserve la "forme" de l'original monad, donc la même chose se produit pour l'expression yield: un List reste un List avec le contenu transformé par l'opération dans le yield.

D'un autre côté, chaque ligne de liaison dans le for n'est qu'une composition de monads successifs, qui doivent être "aplatis" pour conserver une seule "forme externe".

Supposons un instant que chaque liaison interne ait été traduite en un appel à map, mais que la droite était la même fonction _A => M[B]_, vous vous retrouveriez avec un _M[M[B]]_ pour chaque ligne de la compréhension.
L'intention de l'ensemble de la syntaxe for est d'aplanir facilement la concaténation des opérations monadiques successives (c'est-à-dire les opérations qui "soulèvent" une valeur sous une "forme monadique": _A => M[B]_), avec l'ajout d'une opération map finale qui éventuellement effectue une transformation de conclusion.

J'espère que cela explique la logique du choix de la traduction, qui est appliquée de manière mécanique, à savoir: nflatMap appels imbriqués conclus par un seul appel map.

n exemple illustratif artificiel
Destiné à montrer l'expressivité de la syntaxe for

_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 de monad est maintenue grâce à la compréhension, donc nous commençons par List dans _company.branches_, et nous devons terminer par List.
Le type interne change à la place et est déterminé par l'expression yield: qui est _customer.value: Int_

valueList doit être un _List[Int]_

184
pagoda_5b

Je ne suis pas un scala méga esprit alors n'hésitez pas à me corriger, mais voici comment je m'explique la saga flatMap/map/for-comprehension!

Pour comprendre for comprehension Et sa traduction en scala's map / flatMap, Nous devons faire de petites étapes et comprendre les parties de composition - map et flatMap. Mais scala's flatMap N'est pas seulement map avec flatten vous vous demandez! si c'est le cas, pourquoi tant de développeurs ont-ils tant de mal à en saisir le contenu ou à for-comprehension / flatMap / map. Eh bien, si vous regardez simplement les signatures map et flatMap de scala, vous voyez qu'elles retournent le même type de retour M[B] Et qu'elles 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. Comprendre le map de scala.
  2. Comprendre le flatMap de scala.
  3. Comprenez le for comprehension De scala. `

carte de Scala

signature de la carte scala:

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

Mais il y a une grande partie manquante quand on regarde cette signature, et c'est - d'où vient ce A? notre conteneur est de type A il est donc important de regarder cette fonction dans le contexte du conteneur - M[A]. Notre conteneur pourrait être un List d'éléments de type A et notre fonction map prend une fonction qui transforme chaque élément de type A en type B, puis il 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 très important sur la carte - il regroupe automatiquement dans le conteneur de sortie M[B] Vous n'avez aucun contrôle sur celui-ci. Soulignons-le à nouveau:

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

Vous voyez que vous n'avez pas spécifié comment containerize l'élément que vous venez de spécifier comment transformer les éléments internes. Et comme nous avons le même conteneur M pour M[A] Et M[B] Cela signifie que M[B] Est le même conteneur, ce qui signifie que si vous avez List[A] alors vous allez avoir un List[B] et surtout map le fait pour vous!

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

flatMap 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 flatMap dans flatMap, nous lui fournissons la fonction qui non seulement convertit de A to B Mais la conteneurise également dans M[B].

pourquoi on se soucie de qui fait la conteneurisation?

Alors, pourquoi nous soucions-nous tant de la fonction d'entrée pour map/flatMap la conteneurisation en M[B] Ou la carte elle-même effectue la conteneurisation pour nous?

Vous voyez dans le contexte de for comprehension Ce qui se passe, c'est de multiples transformations sur l'article fourni dans le for donc nous donnons au prochain travailleur de notre ligne d'assemblage la possibilité de déterminer l'emballage. Imaginez que nous ayons une chaîne de montage, chaque travailleur fait quelque chose au produit et seul le dernier travailleur le conditionne dans un conteneur! bienvenue dans flatMap c'est son but, dans map chaque travailleur une fois qu'il a fini de travailler sur l'élément le conditionne également pour que vous obteniez des conteneurs sur les conteneurs.

Le puissant pour la compréhension

Maintenant, examinons 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. mkMatcher renvoie un container le conteneur contient une fonction: String => Boolean
  2. Les règles sont les suivantes: si nous avons plusieurs <-, Elles se traduisent par flatMap à l'exception du dernier.
  3. Comme f <- mkMatcher(pat) est le premier dans sequence (pensez Assembly line) Tout ce que nous voulons en sortir est de prendre f et de le passer au prochain travailleur dans le Ligne d'assemblage, nous laissons au travailleur suivant de notre ligne d'assemblage (la fonction suivante) la possibilité de déterminer quel serait le conditionnement de notre article, c'est pourquoi la dernière fonction est map.
  4. La dernière g <- mkMatcher(pat2) utilisera map c'est parce que sa dernière ligne d'assemblage! donc il peut juste faire l'opération finale avec map( g => qui oui! extrait g et utilise le f qui a déjà été extrait du conteneur par le flatMap donc nous nous retrouvons avec d'abord:

    mkMatcher (pat) flatMap (f // extraire la fonction f donne l'élément au prochain travailleur de la chaîne d'assemblage (vous voyez qu'il a accès à f, et ne le reconditionnez pas, je veux dire que la carte détermine l'emballage, laissez le Le travailleur suivant de la chaîne d'assemblage détermine le conteneur. mkMatcher (pat2) map (g => f(s) ...)) // car il s'agit de la dernière fonction de la chaîne d'assemblage que nous allons utiliser pour utiliser la carte et retirer g du conteneur et de l'emballage, son map et cet emballage va étrangler tout le long et sera notre emballage ou notre conteneur, yah!

5
Tomer Ben David

La raison d'être est d'enchaîner les opérations monadiques, ce qui offre comme avantage une gestion des erreurs "échouant rapidement".

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

L'application de la fonction map ou flatMap à un None renvoie toujours un None - la fonction passée en paramètre à map et flatMap n'est pas évalué.

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

Il s'agit du 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 fonction patterns est un moyen typique de "traduire" une gestion impérative des erreurs de style (try...catch) en une gestion des erreurs de style monadique en utilisant Option

4
Bruno Grieder

Cela peut être traduit comme suit:

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

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 pat et foreach élément map à chaque élément dans pat2

1
korefn

Tout d'abord, mkMatcher renvoie une fonction dont la signature est String => Boolean, C'est une procédure Java qui exécute simplement Pattern.compile(string), comme indiqué dans le pattern fonction. Ensuite, regardez cette ligne

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

La fonction map est appliquée au résultat de pattern, qui est Option[Pattern], Donc le p dans p => xxx Est juste le motif que vous compilé. Donc, étant donné un modèle p, une nouvelle fonction est construite, qui prend une chaîne s, et vérifie si s correspond au modèle.

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

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

Voyons maintenant la fonction bothMatch, qui est basée sur mkMatcher. Pour montrer comment bothMathch fonctionne, nous examinons d'abord cette partie:

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

Puisque nous avons obtenu une fonction avec la signature String => Boolean De mkMatcher, qui est g dans ce contexte, g(s) est équivalent à Pattern.compile(pat2).macher(s).matches , qui renvoie si la chaîne s correspond au modèle pat2. Alors que diriez-vous de f(s), c'est la même chose que g(s), la seule différence est que, le premier appel de mkMatcher utilise 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 map pour les deux appels, ce n'est pas ce que vous voulez.

0
xiaowl