Verwechselt mit dem Verständnis der flatMap / Map-Transformation


87

Ich verstehe Map und FlatMap wirklich nicht. Was ich nicht verstehe, ist, wie ein For-Understanding eine Folge verschachtelter Aufrufe von map und flatMap ist. Das folgende Beispiel stammt aus der funktionalen Programmierung in Scala

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

wird übersetzt in

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

Die mkMatcher-Methode ist wie folgt definiert:

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

Und die Mustermethode ist wie folgt:

import java.util.regex._

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

Es wäre großartig, wenn jemand etwas Licht in die Gründe für die Verwendung von map und flatMap bringen könnte.

Antworten:


197

TL; DR gehen direkt zum letzten Beispiel

Ich werde versuchen, es noch einmal zusammenzufassen.

Definitionen

Das forVerständnis ist eine Syntax Verknüpfung zu kombinieren flatMapund mapin einer Weise , die über leicht zu lesen und Grund.

Vereinfachen wir die Dinge ein wenig und nehmen an, dass jede class, die beide oben genannten Methoden bereitstellt, als a bezeichnet werden kann, monadund wir verwenden das Symbol M[A], um a monadmit einem inneren Typ zu bezeichnen A.

Beispiele

Einige häufig gesehene Monaden sind:

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

map und flatMap

In einer generischen Monade definiert 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]

z.B

  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)

zum Ausdruck bringen

  1. Jede Zeile im Ausdruck, die das <-Symbol verwendet, wird in einen flatMapAufruf übersetzt, mit Ausnahme der letzten Zeile, die in einen abschließenden mapAufruf übersetzt wird, in der das "gebundene Symbol" auf der linken Seite als Parameter an die Argumentfunktion übergeben wird (was wir haben vorher angerufen 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. Ein for-Ausdruck mit nur einem <-wird in einen mapAufruf mit dem als Argument übergebenen Ausdruck konvertiert :

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

Nun zum Punkt

Wie Sie sehen können, behält die mapOperation die "Form" des Originals bei monad, so dass dies auch für den yieldAusdruck gilt: a Listbleibt a Listmit dem durch die Operation in der Datei transformierten Inhalt yield.

Andererseits ist jede Bindungslinie in der fornur eine Zusammensetzung von aufeinanderfolgenden monads, die "abgeflacht" werden muss, um eine einzelne "äußere Form" beizubehalten.

Angenommen, für einen Moment wurde jede interne Bindung in einen mapAufruf übersetzt, aber die rechte Hand war dieselbe A => M[B]Funktion. Sie würden M[M[B]]für jede Zeile im Verständnis eine erhalten.
Die Absicht der gesamten forSyntax besteht darin, die Verkettung aufeinanderfolgender monadischer Operationen (dh Operationen, die einen Wert in einer "monadischen Form" "anheben") leicht zu "reduzieren" A => M[B], wobei eine letzte mapOperation hinzugefügt wird , die möglicherweise eine abschließende Transformation durchführt.

Ich hoffe, dies erklärt die Logik hinter der Wahl der Übersetzung, die auf mechanische Weise angewendet wird, n flatMapdh verschachtelte Anrufe, die durch einen einzelnen mapAnruf abgeschlossen werden.

Ein erfundenes anschauliches Beispiel soll
die Ausdruckskraft der forSyntax demonstrieren

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

Können Sie die Art von erraten valuesList?

Wie bereits gesagt, wird die Form des monaddurch das Verständnis beibehalten, also beginnen wir mit einem ListIn company.branchesund müssen mit einem enden List.
Der innere Typ ändert sich stattdessen und wird durch den yieldAusdruck bestimmt: welcher istcustomer.value: Int

valueList sollte ein sein List[Int]


1
Die Wörter "ist dasselbe wie" gehören zur Metasprache und sollten aus dem Codeblock entfernt werden.
Tag

3
Jeder FP-Anfänger sollte dies lesen. Wie kann dies erreicht werden?
Mert Inan

1
@melston Lass uns ein Beispiel machen mit Lists. Wenn Sie mapzweimal eine Funktion A => List[B](die eine der wesentlichen monadischen Operationen ist) über einen bestimmten Wert ausführen, erhalten Sie eine Liste [Liste [B]] (wir gehen davon aus, dass die Typen übereinstimmen). Die zum Verständnis innere Schleife setzt diese Funktionen mit der entsprechenden flatMapOperation zusammen und "glättet" die Form der Liste [Liste [B]] in eine einfache Liste [B] ... Ich hoffe, das ist klar
pagoda_5b

1
Es war einfach großartig, Ihre Antwort zu lesen. Ich wünschte du würdest ein Buch über Scala schreiben, hast du einen Blog oder so?
Tomer Ben David

1
@coolbreeze Es könnte sein, dass ich es nicht klar ausgedrückt habe. Was ich damit gemeint habe ist, dass die yieldKlausel customer.value, deren Typ ist Int, daher das Ganze zu a for comprehensionbewertet List[Int].
pagoda_5b

6

Ich bin kein Scala-Mega-Geist, also zögern Sie nicht, mich zu korrigieren, aber so erkläre flatMap/map/for-comprehensionich mir die Saga!

Um zu verstehen for comprehensionund es zu übersetzen, scala's map / flatMapmüssen wir kleine Schritte unternehmen und die komponierenden Teile verstehen - mapund flatMap. Aber ist nicht scala's flatMapnur mapbei flattendir, frag dich! wenn ja , warum so viele Entwickler finden es so schwer , das Verständnis für sie zu bekommen oder von for-comprehension / flatMap / map. Wenn Sie sich nur Scalas mapund flatMapSignaturen ansehen, sehen Sie, dass sie denselben Rückgabetyp zurückgeben M[B]und mit demselben Eingabeargument arbeiten A(zumindest den ersten Teil der Funktion, die sie übernehmen), wenn dies den Unterschied ausmacht.

Unser Plan

  1. Scala verstehen map.
  2. Scala verstehen flatMap.
  3. Verstehe for comprehensionScalas. "

Scalas Karte

Scala-Kartensignatur:

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

Aber es fehlt ein großer Teil, wenn wir uns diese Signatur ansehen, und es ist - woher kommt das A? Unser Container ist vom Typ, Adaher ist es wichtig, diese Funktion im Kontext des Containers zu betrachten M[A]. Unser Container kann ein ListElement vom Typ sein, Aund unsere mapFunktion verwendet eine Funktion, die jedes Element vom Typ Ain einen Typ umwandelt. BAnschließend wird ein Container vom Typ B(oder M[B]) zurückgegeben.

Schreiben wir die Signatur der Karte unter Berücksichtigung des Containers:

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]

Beachten Sie eine äußerst wichtige Tatsache in Bezug auf die Karte : Sie wird automatisch in dem Ausgabecontainer gebündelt , über den M[B]Sie keine Kontrolle haben. Lassen Sie es uns noch einmal betonen:

  1. mapwählt den Ausgabecontainer für uns und er wird derselbe Container sein wie die Quelle, an der wir arbeiten. Für den M[A]Container erhalten wir den gleichen MContainer nur für B M[B]und sonst nichts!
  2. mapWenn wir diese Containerisierung für uns durchführen, geben wir nur ein Mapping von Abis an Bund es würde es in die Box von M[B]wird es für uns in die Box legen!

Sie sehen, Sie haben nicht angegeben, wie containerizedas Element, das Sie gerade angegeben haben, wie die internen Elemente transformiert werden sollen. Und da wir Mfür beide den gleichen Container haben M[A]und M[B]dies bedeutet, dass M[B]es sich um den gleichen Container handelt, bedeutet dies, dass Sie einen List[A]haben List[B]und, was noch wichtiger mapist, dies für Sie tun wird!

Nun, da wir uns damit befasst haben, gehen wir mapweiter zu flatMap.

Scalas flatMap

Mal sehen, seine Unterschrift:

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

Sie sehen den großen Unterschied zwischen Map und flatMapFlatMap. Wir bieten ihm die Funktion, die ihn nicht nur konvertiert, A to Bsondern auch in Container umwandelt M[B].

Warum interessiert es uns, wer die Containerisierung durchführt?

Warum kümmern wir uns so sehr um die Eingabefunktion für map / flatMap, die Containerisierung in M[B]oder die Karte selbst übernimmt die Containerisierung für uns?

Sie sehen im Zusammenhang mit dem, for comprehensionwas passiert, dass mehrere Transformationen an dem im bereitgestellten Artikel stattfinden, forsodass wir dem nächsten Mitarbeiter in unserer Montagelinie die Möglichkeit geben, die Verpackung zu bestimmen. Stellen Sie sich vor, wir haben eine Montagelinie, an der jeder Arbeiter etwas mit dem Produkt macht und nur der letzte Arbeiter verpackt es in einem Behälter! Willkommen zu flatMapdiesem Zweckmap jeder Arbeiter, wenn er mit der Arbeit an dem Gegenstand fertig auch verpackt, so dass Sie Container über Container bekommen.

Die Mächtigen zum Verständnis

Lassen Sie uns nun Ihr Verständnis untersuchen und dabei berücksichtigen, was wir oben gesagt haben:

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

Was haben wir hier:

  1. mkMatcherGibt a zurück, containerder Container enthält eine Funktion:String => Boolean
  2. Die Regeln sind die, wenn wir mehrere haben, in die <-sie übersetzt werdenflatMap Ausnahme der letzten.
  3. Als f <- mkMatcher(pat)erstes in ist sequence(denken assembly line) alles , was wir wollen aus er heraus zu nehmen ist fund es auf den nächsten Arbeiter in der Montagelinie passieren, wir den nächsten Arbeiter in unserer Montagelinie (die nächste Funktion) die Fähigkeit lassen , um festzustellen , was das sein würde , Verpackung zurück von unserem Artikel ist deshalb die letzte Funktion map.
  4. Der letzte g <- mkMatcher(pat2)wird mapdies verwenden, weil es das letzte am Fließband ist! so kann es nur die letzte Operation machen mit map( g =>der ja! zieht aus gund verwendet das, fwas bereits aus dem Behälter herausgezogen wurde, flatMapdaher erhalten wir zuerst:

    mkMatcher (pat) flatMap (f // Funktion herausziehen f Element an nächsten Fließbandarbeiter weitergeben (Sie sehen, es hat Zugriff auf fund verpacken es nicht zurück Ich meine, lassen Sie die Karte die Verpackung bestimmen, lassen Sie den nächsten Fließbandarbeiter bestimmen container. mkMatcher (pat2) map (g => f (s) ...)) // da dies die letzte Funktion in der Montagelinie ist, werden wir map verwenden und g aus dem Container und zurück zur Verpackung ziehen , seine mapund diese Verpackung werden den ganzen Weg drosseln und unser Paket oder unser Container sein, yah!


4

Das Grundprinzip besteht darin, monadische Operationen zu verketten, was als Vorteil eine ordnungsgemäße "schnelle Fehlerbehandlung" bietet.

Es ist eigentlich ziemlich einfach. Die mkMatcherMethode gibt eine Option(die eine Monade ist) zurück. Das Ergebnis mkMatcherder monadischen Operation ist entweder a Noneoder a Some(x).

Das Anwenden der Funktion mapoder flatMapauf a gibt Noneimmer a zurück None- die Funktion, die als Parameter an übergeben mapund flatMapnicht ausgewertet wird.

Wenn in Ihrem Beispiel mkMatcher(pat)eine None zurückgegeben wird, gibt die darauf angewendete flatMap a zurück None(die zweite monadische Operation mkMatcher(pat2)wird nicht ausgeführt), und die letzte Operation mapgibt erneut a zurück None. Mit anderen Worten, wenn eine der Operationen zum Verständnis eine Keine zurückgibt, haben Sie ein ausfallsicheres Verhalten und der Rest der Operationen wird nicht ausgeführt.

Dies ist der monadische Stil der Fehlerbehandlung. Der imperative Stil verwendet Ausnahmen, die im Grunde genommen Sprünge sind (zu einer catch-Klausel).

Ein letzter Hinweis: Die patternsFunktion ist eine typische Methode zum "Übersetzen" einer imperativen Stilfehlerbehandlung ( try... catch) in eine monadische Stilfehlerbehandlung mitOption


Wissen Sie, warum flatMap(und nicht map) verwendet wird, um den ersten und den zweiten Aufruf von "zu verketten" mkMatcher, aber warum map(und nicht flatMap) verwendet wird, um den zweiten mkMatcherund den yieldsBlock zu "verketten" ?
Malte Schwerhoff

1
flatMaperwartet, dass Sie eine Funktion übergeben, die das in der Monade "eingewickelte" / angehobene Ergebnis zurückgibt, während mapSie das Umhüllen / Anheben selbst durchführen. Während der Aufrufverkettung von Operationen in müssen for comprehensionSie, flatmapdamit die als Parameter übergebenen Funktionen zurückkehren können None(Sie können den Wert nicht in None setzen). Es yieldwird erwartet , dass der letzte Operationsaufruf, der im ausgeführt wird, ausgeführt wird und einen Wert zurückgibt. a, mapum die letzte Operation zu verketten, ist ausreichend und vermeidet, dass das Ergebnis der Funktion in die Monade gehoben werden muss.
Bruno Grieder

1

Dies kann wie folgt übersetzt werden:

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)

Führen Sie dies aus, um eine bessere Übersicht über die Erweiterung zu erhalten

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)

Ergebnisse sind:

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

Dies ähnelt einer flatMapSchleife durch jedes Element in patund leitet jedes Element mapan jedes Element in weiterpat2


0

Erstens mkMatchergibt eine Funktion , deren Signatur ist String => Boolean, die eine reguläre Java - Prozedur ist , die gerade ausgeführt werden Pattern.compile(string), wie in der gezeigte patternFunktion. Dann schauen Sie sich diese Zeile an

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

Die mapFunktion wird auf das Ergebnis angewandt pattern, das ist Option[Pattern], so dass die pin p => xxxgenau das Muster , das Sie zusammengestellt. Wenn ein Muster gegeben ist p, wird eine neue Funktion erstellt, die einen String akzeptiert sund prüft, ob sie smit dem Muster übereinstimmt.

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

Beachten Sie, dass die pVariable an das kompilierte Muster gebunden ist. Nun ist klar, wie eine Funktion mit Signatur aufgebaut String => BooleanistmkMatcher .

Als nächstes überprüfen wir die bothMatchFunktion, auf der basiert mkMatcher. Um zu zeigen, wie es bothMathchfunktioniert, schauen wir uns zuerst diesen Teil an:

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

Da wir eine Funktion mit Signatur String => Booleanvon erhalten haben mkMatcher, die gin diesem Zusammenhang g(s)äquivalent zu ist Pattern.compile(pat2).macher(s).matches, wird zurückgegeben, wenn der String mit dem Muster übereinstimmt pat2. Wie wäre f(s)es also mit g(s)dem einzigen Unterschied, dass der erste Aufruf der mkMatcherVerwendung flatMapstatt mapWarum? Da mkMatcher(pat2) map (g => ....)zurückgegeben wird Option[Boolean], erhalten Sie ein verschachteltes Ergebnis, Option[Option[Boolean]]wenn Sie mapbeide Aufrufe verwenden. Dies ist nicht das, was Sie möchten.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.