TL; DR przejdź bezpośrednio do ostatniego przykładu
Spróbuję podsumować.
Definicje
for
Zrozumienie jest skrótem składnia łączyć flatMap
i map
w sposób, który jest łatwy do odczytania i powodem około.
Uprośćmy trochę sprawę i załóżmy, że każdy, class
który zapewnia obie powyższe metody, można nazwać a, monad
a użyjemy symbolu M[A]
do oznaczenia a monad
z typem wewnętrznym A
.
Przykłady
Niektóre powszechnie spotykane monady to:
List[String]
gdzie
M[X] = List[X]
A = String
Option[Int]
gdzie
Future[String => Boolean]
gdzie
M[X] = Future[X]
A = (String => Boolean)
map i flatMap
Zdefiniowany w ogólnej monadzie M[A]
def map(f: A => B): M[B]
def flatMap(f: A => M[B]): M[B]
na przykład
val list = List("neo", "smith", "trinity")
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
Każda linia w wyrażeniu używającym <-
symbolu jest tłumaczona na flatMap
wywołanie, z wyjątkiem ostatniej linii, która jest tłumaczona na map
wywoł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]
):
for {
bound <- list
out <- f(bound)
} yield out
list.flatMap { bound =>
f(bound).map { out =>
out
}
}
list.flatMap { bound =>
f(bound)
}
list flatMap f
Wyrażenie for z tylko jednym <-
jest konwertowane na map
wywołanie z wyrażeniem przekazanym jako argument:
for {
bound <- list
} yield f(bound)
list.map { bound =>
f(bound)
}
list map f
Teraz do rzeczy
Jak widać, map
operacja zachowuje „kształt” oryginału monad
, więc to samo dzieje się z yield
wyrażeniem: a List
pozostaje a List
z treścią przekształconą przez operację w yield
.
Z drugiej strony każda linia oprawy for
jest 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 map
wywoł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 for
skł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 map
operacji, 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
flatMap
połączenia zagnieżdżone zakończone pojedynczym map
wywołaniem.
Wymyślony przykład ilustrujący.
Ma na celu pokazanie ekspresji for
skł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 monad
jest zachowywany podczas rozumienia, więc zaczynamy od List
in company.branches
i musimy kończyć na List
.
Zamiast tego typ wewnętrzny zmienia się i jest określany przez yield
wyrażenie: który jestcustomer.value: Int
valueList
powinien być List[Int]
Lists
. Jeślimap
podwoisz 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ąflatMap
operacją, "spłaszczając" kształt listy [List [B]] w prostą listę [B] ... Mam nadzieję, że to jest jasneyield
klauzula tocustomer.value
, której typ jestInt
, dlatego całośćfor comprehension
zwraca się do aList[Int]
.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-comprehension
sagę!Aby zrozumieć
for comprehension
i to przetłumaczyćscala's map / flatMap
, musimy podjąć małe kroki i zrozumieć części składowe -map
iflatMap
. Ale niescala's flatMap
tylkomap
zflatten
tobą, 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 scalamap
iflatMap
podpis, zobaczysz, że zwracają ten sam typ powrotuM[B]
i działają na tym samym argumencie wejściowymA
(przynajmniej na pierwszej części funkcji, którą przyjmują). Jeśli tak, to co robi różnicę?Nasz plan
map
.flatMap
.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
A
bierze? nasz kontener jest tego typu,A
więc ważne jest, aby spojrzeć na tę funkcję w kontekście kontenera -M[A]
. Nasz kontener może być elementemList
typu,A
a naszamap
funkcja przyjmuje funkcję, która przekształca każdy element typuA
na typB
, a następnie zwraca kontener typuB
(lubM[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:map
wybiera dla nas kontener wyjściowy i będzie to ten sam kontener co źródło, nad którym pracujemy, więc dlaM[A]
kontenera otrzymujemy ten samM
kontener tylkoB
M[B]
i nic więcej!map
robi to dla nas konteneryzacja, po prostu podajemy mapowanie odA
doB
i 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
containerize
sposób chcesz zmienić element, który właśnie określiłeś, jak przekształcić elementy wewnętrzne. A ponieważ mamy ten sam pojemnikM
dla obu,M[A]
aM[B]
to oznacza, żeM[B]
jest to ten sam pojemnik, co oznacza, że jeśli masz,List[A]
będziesz miećList[B]
i co ważniejsze,map
zrobisz to za Ciebie!Teraz, kiedy już mamy do czynienia,
map
przejdźmy doflatMap
.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
flatMap
flatMap, którą dostarczamy z funkcją, która nie tylko konwertuje,A to B
ale także kontenerujeM[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 comprehension
tego, co się dzieje, to wielokrotne przekształcenia na dostarczonym elemencie,for
wię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 wflatMap
tym celu,map
każ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:
mkMatcher
zwracacontainer
kontener zawierający funkcję:String => Boolean
<-
które przekładają się zflatMap
wyjątkiem ostatniego.f <- mkMatcher(pat)
jest to pierwsze wsequence
(pomyślassembly line
) wszystko, czego chcemy, to zabraćf
i 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 jestmap
.Ostatni
g <- mkMatcher(pat2)
skorzysta zmap
tego, ponieważ ostatni na linii montażowej! więc może po prostu wykonać ostatnią operację, zmap( g =>
którą tak! wyciągag
i używa tego,f
który został już wyciągnięty z pojemnika przezflatMap
co 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
f
i 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 , jegomap
i to opakowanie będzie się dławić i stanie się naszą paczką lub naszym pojemnikiem, tak!źródło
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.
mkMatcher
Metody zwracającejOption
(który jest Monada). WynikiemmkMatcher
operacji monadycznej jest aNone
lub aSome(x)
.Zastosowanie funkcji
map
orflatMap
do aNone
zawsze zwracaNone
- funkcję przekazaną jako parametr domap
iflatMap
nie jest oceniana.Stąd w twoim przykładzie, jeśli
mkMatcher(pat)
zwróci None, zastosowana do niej flatMap zwróci aNone
(druga operacja monadycznamkMatcher(pat2)
nie zostanie wykonana), a finałmap
ponownie zwróci aNone
. 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:
patterns
funkcja 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
źródło
flatMap
(i niemap
) jest używane do „konkatenacji” pierwszego i drugiego wywołaniamkMatcher
, ale dlaczegomap
(i nieflatMap
) jest używane do „łączenia” drugiegomkMatcher
iyields
bloku?flatMap
oczekuje, że przekażesz funkcję zwracającą wynik „zawinięty” / podniesiony w monadzie, podczas gdy sammap
zrobi zawijanie / podnoszenie. Podczas łączenia operacji w łańcuchu wywołańfor comprehension
należy, abyflatmap
funkcje przekazane jako parametr mogły zwracaćNone
(nie można podnieść wartości do None).yield
Oczekuje się, że ostatnie wywołanie operacji, ta w elemencie, zostanie uruchomione i zwróci wartość; amap
do łańcucha ta ostatnia operacja jest wystarczająca i pozwala uniknąć konieczności podnoszenia wyniku funkcji do monady.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 inpat
imap
na każdym elemencie do każdego elementu wpat2
źródło
Po pierwsze,
mkMatcher
zwraca funkcję, której sygnatura toString => Boolean
zwykła procedura java, która właśnie została uruchomionaPattern.compile(string)
, jak pokazano wpattern
funkcji. Następnie spójrz na tę liniępattern(pat) map (p => (s:String) => p.matcher(s).matches)
map
Funkcja jest stosowana do wynikupattern
, który jestOption[Pattern]
, więcp
wp => xxx
to po prostu wzór został skompilowany. Tak więc, mając wzorzecp
, konstruowana jest nowa funkcja, która pobiera Strings
i sprawdza, czys
pasuje do wzorca.(s: String) => p.matcher(s).matches
Zauważ, że
p
zmienna jest ograniczona do skompilowanego wzorca. Teraz jest jasne, że w jaki sposóbString => Boolean
jest konstruowana funkcja z podpisemmkMatcher
.Następnie sprawdźmy
bothMatch
funkcję, na której bazujemkMatcher
. Aby pokazać, jakbothMathch
działa, najpierw przyjrzymy się tej części:Ponieważ otrzymaliśmy funkcję z sygnaturą
String => Boolean
odmkMatcher
, która jestg
w tym kontekścieg(s)
równoważnaPattern.compile(pat2).macher(s).matches
, która zwraca, jeśli String s pasuje do wzorcapat2
. Więc co powiesz naf(s)
to samog(s)
, jedyną różnicą jest to, że pierwsze wywołaniemkMatcher
używaflatMap
zamiast „map
Dlaczego? PonieważmkMatcher(pat2) map (g => ....)
zwracaOption[Boolean]
, otrzymasz zagnieżdżony wynik,Option[Option[Boolean]]
jeśli użyjeszmap
obu wywołań, a nie tego chcesz.źródło