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.
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
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
_
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: n
flatMap
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]
_
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
map
de scala.flatMap
de scala.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:
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 B
M[B]
et rien d'autre!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:
mkMatcher
renvoie un container
le conteneur contient une fonction: String => Boolean
<-
, Elles se traduisent par flatMap
à l'exception du dernier.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
.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!
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
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
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.