Mylony z przekształceniem dla zrozumienia w płaską mapę / mapę

87

Naprawdę nie rozumiem Map i FlatMap. To, czego nie rozumiem, to jak interpretacja jest sekwencją zagnieżdżonych wywołań map i flatMap. Poniższy przykład pochodzi z programowania funkcjonalnego w Scali

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

przetłumaczyć na

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

Metoda mkMatcher jest zdefiniowana w następujący sposób:

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

A metoda wzorców jest następująca:

import java.util.regex._

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

Byłoby wspaniale, gdyby ktoś mógł rzucić trochę światła na uzasadnienie używania map i flatMap tutaj.

sc_ray
źródło

Odpowiedzi:

200

TL; DR przejdź bezpośrednio do ostatniego przykładu

Spróbuję podsumować.

Definicje

forZrozumienie jest skrótem składnia łączyć flatMapi mapw sposób, który jest łatwy do odczytania i powodem około.

Uprośćmy trochę sprawę i załóżmy, że każdy, classktóry zapewnia obie powyższe metody, można nazwać a, monada użyjemy symbolu M[A]do oznaczenia a monadz typem wewnętrznym A.

Przykłady

Niektóre powszechnie spotykane monady to:

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

map i flatMap

Zdefiniowany w ogólnej monadzie 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]

na przykład

  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)

do wyrażenia

  1. Każda linia w wyrażeniu używającym <-symbolu jest tłumaczona na flatMapwywołanie, z wyjątkiem ostatniej linii, która jest tłumaczona na mapwywołanie końcowe , gdzie „symbol związany” po lewej stronie jest przekazywany jako parametr funkcji argumentu (co wcześniej dzwoniliśmy 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. Wyrażenie for z tylko jednym <-jest konwertowane na mapwywołanie z wyrażeniem przekazanym jako 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
    

Teraz do rzeczy

Jak widać, mapoperacja zachowuje „kształt” oryginału monad, więc to samo dzieje się z yieldwyrażeniem: a Listpozostaje a Listz treścią przekształconą przez operację w yield.

Z drugiej strony każda linia oprawy forjest tylko kompozycją kolejnych monads, które muszą być „spłaszczone”, aby zachować jeden „kształt zewnętrzny”.

Załóżmy przez chwilę, że każde wewnętrzne powiązanie zostało przetłumaczone na mapwywołanie, ale prawa ręka była tą samą A => M[B]funkcją, skończysz z znakiem M[M[B]]dla każdego wiersza w zrozumieniu.
Celem całej forskładni jest łatwe „spłaszczenie” konkatenacji kolejnych operacji monadycznych (tj. Operacji, które „podnoszą” wartość w „kształt monadyczny”:) A => M[B], z dodaniem końcowej mapoperacji, która prawdopodobnie wykonuje przekształcenie końcowe.

Mam nadzieję, że wyjaśnia to logikę wyboru tłumaczenia, które jest stosowane w sposób mechaniczny, to znaczy: n flatMappołączenia zagnieżdżone zakończone pojedynczym mapwywołaniem.

Wymyślony przykład ilustrujący.
Ma na celu pokazanie ekspresji forskładni

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

Czy potrafisz odgadnąć rodzaj valuesList?

Jak już powiedziano, kształt litery monadjest zachowywany podczas rozumienia, więc zaczynamy od Listin company.branchesi musimy kończyć na List.
Zamiast tego typ wewnętrzny zmienia się i jest określany przez yieldwyrażenie: który jestcustomer.value: Int

valueList powinien być List[Int]

pagoda_5b
źródło
1
Słowa „to to samo, co” należą do metajęzyka i powinny zostać usunięte z bloku kodu.
dzień
3
Każdy początkujący gracz FP powinien to przeczytać. Jak można to osiągnąć?
mert inan
1
@melston Zróbmy przykład z Lists. Jeśli mappodwoisz funkcję A => List[B](która jest jedną z podstawowych operacji monadycznych) nad pewną wartością, otrzymasz List [List [B]] (zakładamy, że typy pasują do siebie). Wewnętrzna pętla do zrozumienia komponuje te funkcje z odpowiednią flatMapoperacją, "spłaszczając" kształt listy [List [B]] w prostą listę [B] ... Mam nadzieję, że to jest jasne
pagoda_5b
1
Twoja odpowiedź była niesamowita. Chciałbym, żebyś napisał książkę o scali, masz bloga czy coś?
Tomer Ben David
1
@coolbreeze Możliwe, że nie wyraziłem tego jasno. Chodziło mi o to, że yieldklauzula to customer.value, której typ jest Int, dlatego całość for comprehensionzwraca się do a List[Int].
pagoda_5b
7

Nie jestem mega umysłem scala, więc nie krępuj się, aby mnie poprawić, ale w ten sposób wyjaśniam sobie tę flatMap/map/for-comprehensionsagę!

Aby zrozumieć for comprehensioni to przetłumaczyć scala's map / flatMap, musimy podjąć małe kroki i zrozumieć części składowe - mapi flatMap. Ale nie scala's flatMaptylko mapz flattentobą, zapytaj siebie! jeśli tak, dlaczego tak wielu programistom jest tak trudno pojąć to lub pojąć for-comprehension / flatMap / map. Cóż, jeśli spojrzysz tylko na scala mapi flatMappodpis, zobaczysz, że zwracają ten sam typ powrotu M[B]i działają na tym samym argumencie wejściowym A(przynajmniej na pierwszej części funkcji, którą przyjmują). Jeśli tak, to co robi różnicę?

Nasz plan

  1. Zrozum Scala's map.
  2. Zrozum Scala's flatMap.
  3. Zrozum Scala's. for comprehension"

Mapa Scali

sygnatura mapy scala:

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

Ale kiedy patrzymy na ten podpis, brakuje dużej części, a to - skąd się to Abierze? nasz kontener jest tego typu, Awięc ważne jest, aby spojrzeć na tę funkcję w kontekście kontenera - M[A]. Nasz kontener może być elementem Listtypu, Aa nasza mapfunkcja przyjmuje funkcję, która przekształca każdy element typu Ana typ B, a następnie zwraca kontener typu B(lub M[B])

Napiszmy sygnaturę mapy uwzględniającą kontener:

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]

Zwróć uwagę na niezwykle bardzo ważny fakt dotyczący mapy - jest ona umieszczana automatycznie w pojemniku wyjściowym M[B], nad którym nie masz kontroli. Podkreślmy to jeszcze raz:

  1. mapwybiera dla nas kontener wyjściowy i będzie to ten sam kontener co źródło, nad którym pracujemy, więc dla M[A]kontenera otrzymujemy ten sam Mkontener tylko B M[B]i nic więcej!
  2. maprobi to dla nas konteneryzacja, po prostu podajemy mapowanie od Ado Bi umieścimy go w pudełku, M[B]a umieścimy go w pudełku za nas!

Widzisz, że nie określiłeś, w jaki containerizesposób chcesz zmienić element, który właśnie określiłeś, jak przekształcić elementy wewnętrzne. A ponieważ mamy ten sam pojemnik Mdla obu, M[A]a M[B]to oznacza, że M[B]jest to ten sam pojemnik, co oznacza, że ​​jeśli masz, List[A]będziesz mieć List[B]i co ważniejsze, mapzrobisz to za Ciebie!

Teraz, kiedy już mamy do czynienia, mapprzejdźmy do flatMap.

Mapa mieszkania Scali

Zobaczmy jego podpis:

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

Widzisz dużą różnicę między mapą a flatMapflatMap, którą dostarczamy z funkcją, która nie tylko konwertuje, A to Bale także konteneruje M[B].

dlaczego zależy nam na tym, kto zajmuje się konteneryzacją?

Dlaczego więc tak bardzo zależy nam na funkcji wejściowej do mapowania / flatMap, do której dokonuje się konteneryzacja M[B]lub sama mapa wykonuje konteneryzację za nas?

Widzisz w kontekście for comprehensiontego, co się dzieje, to wielokrotne przekształcenia na dostarczonym elemencie, forwięc dajemy następnemu pracownikowi na naszej linii montażowej możliwość określenia opakowania. wyobraź sobie, że mamy linię montażową, każdy pracownik robi coś z produktem i tylko ostatni pracownik pakuje go do pojemnika! witamy w flatMaptym celu, mapkażdy pracownik po zakończeniu pracy nad przedmiotem również pakuje go, aby uzyskać kontenery nad kontenerami.

Potężny do zrozumienia

Przyjrzyjmy się teraz twojemu zrozumieniu, biorąc pod uwagę to, co powiedzieliśmy powyżej:

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

Co tu mamy:

  1. mkMatcherzwraca containerkontener zawierający funkcję:String => Boolean
  2. Reguły są takie, jeśli mamy wiele, na <-które przekładają się z flatMapwyjątkiem ostatniego.
  3. Ponieważ f <- mkMatcher(pat)jest to pierwsze w sequence(pomyśl assembly line) wszystko, czego chcemy, to zabrać fi przekazać kolejnemu pracownikowi na linii montażowej, pozwalamy następnemu pracownikowi na naszej linii montażowej (kolejnej funkcji) określić, co będzie pakowanie z powrotem naszego przedmiotu, dlatego ostatnia funkcja jest map.
  4. Ostatni g <- mkMatcher(pat2)skorzysta z maptego, ponieważ ostatni na linii montażowej! więc może po prostu wykonać ostatnią operację, z map( g =>którą tak! wyciąga gi używa tego, fktóry został już wyciągnięty z pojemnika przez flatMapco kończymy najpierw:

    mkMatcher (pat) flatMap (f // pull out f function daj element następnemu pracownikowi linii montażowej (widzisz, że ma do niego dostęp fi nie pakuj go z powrotem, to znaczy niech mapa określi opakowanie, niech następny pracownik linii montażowej określi container. mkMatcher (pat2) map (g => f (s) ...)) // ponieważ jest to ostatnia funkcja w linii montażowej, będziemy używać map i wyciągać g z pojemnika i do opakowania z powrotem , jego mapi to opakowanie będzie się dławić i stanie się naszą paczką lub naszym pojemnikiem, tak!

Tomer Ben David
źródło
4

Uzasadnieniem jest połączenie operacji monadycznych w łańcuch, co zapewnia jako korzyść właściwą obsługę błędów „fail fast”.

W rzeczywistości jest to całkiem proste. mkMatcherMetody zwracającej Option(który jest Monada). Wynikiem mkMatcheroperacji monadycznej jest a Nonelub a Some(x).

Zastosowanie funkcji mapor flatMapdo a Nonezawsze zwraca None- funkcję przekazaną jako parametr do mapi flatMapnie jest oceniana.

Stąd w twoim przykładzie, jeśli mkMatcher(pat)zwróci None, zastosowana do niej flatMap zwróci a None(druga operacja monadyczna mkMatcher(pat2)nie zostanie wykonana), a finał mapponownie zwróci a None. Innymi słowy, jeśli którakolwiek z operacji w instrukcji do zrozumienia zwraca wartość None, masz szybkie zachowanie typu fail-fast, a pozostałe operacje nie są wykonywane.

To jest monadyczny styl obsługi błędów. Styl rozkazujący używa wyjątków, które są po prostu skokami (do klauzuli catch)

Ostatnia uwaga: patternsfunkcja jest typowym sposobem "tłumaczenia" imperatywnej obsługi błędów w stylu ( try... catch) na monadyczną obsługę błędów przy użyciuOption

Bruno Grieder
źródło
Czy wiesz, dlaczego flatMap(i nie map) jest używane do „konkatenacji” pierwszego i drugiego wywołania mkMatcher, ale dlaczego map(i nie flatMap) jest używane do „łączenia” drugiego mkMatcheri yieldsbloku?
Malte Schwerhoff
1
flatMapoczekuje, że przekażesz funkcję zwracającą wynik „zawinięty” / podniesiony w monadzie, podczas gdy sam mapzrobi zawijanie / podnoszenie. Podczas łączenia operacji w łańcuchu wywołań for comprehensionnależy, aby flatmapfunkcje przekazane jako parametr mogły zwracać None(nie można podnieść wartości do None). yieldOczekuje się, że ostatnie wywołanie operacji, ta w elemencie, zostanie uruchomione i zwróci wartość; a mapdo łańcucha ta ostatnia operacja jest wystarczająca i pozwala uniknąć konieczności podnoszenia wyniku funkcji do monady.
Bruno Grieder
1

Można to przetranslować jako:

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)

Uruchom to, aby uzyskać lepszy widok na to, jak został rozszerzony

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)

wyniki to:

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

Jest to podobne do flatMap- pętla przechodząca przez każdy element in pati mapna każdym elemencie do każdego elementu wpat2

korefn
źródło
0

Po pierwsze, mkMatcherzwraca funkcję, której sygnatura to String => Booleanzwykła procedura java, która właśnie została uruchomiona Pattern.compile(string), jak pokazano w patternfunkcji. Następnie spójrz na tę linię

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

mapFunkcja jest stosowana do wyniku pattern, który jest Option[Pattern], więc pw p => xxxto po prostu wzór został skompilowany. Tak więc, mając wzorzec p, konstruowana jest nowa funkcja, która pobiera String si sprawdza, czy spasuje do wzorca.

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

Zauważ, że pzmienna jest ograniczona do skompilowanego wzorca. Teraz jest jasne, że w jaki sposób String => Booleanjest konstruowana funkcja z podpisem mkMatcher.

Następnie sprawdźmy bothMatchfunkcję, na której bazuje mkMatcher. Aby pokazać, jak bothMathchdziała, najpierw przyjrzymy się tej części:

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

Ponieważ otrzymaliśmy funkcję z sygnaturą String => Booleanod mkMatcher, która jest gw tym kontekście g(s)równoważna Pattern.compile(pat2).macher(s).matches, która zwraca, jeśli String s pasuje do wzorca pat2. Więc co powiesz na f(s)to samo g(s), jedyną różnicą jest to, że pierwsze wywołanie mkMatcherużywa flatMapzamiast „ mapDlaczego? Ponieważ mkMatcher(pat2) map (g => ....)zwraca Option[Boolean], otrzymasz zagnieżdżony wynik, Option[Option[Boolean]]jeśli użyjesz mapobu wywołań, a nie tego chcesz.

xiaowl
źródło