23 - monádok¶
A funkcionális programozási paradigmában az ember előbb-utóbb találkozik azzal a fogalommal, hogy ,,monád''. Szokták erre mondani, hogy ,,aki megérti, mi az a monád, képtelenné válik arra, hogy elmagyarázza, mi az a monád'', de azért csak próbáljuk meg.
Elöljáróban: a List
az egy monád lesz, mert generikus, mert van neki flatMap
je, mert van egy megfelelő konstruktora
és ezek között teljesülnek bizonyos összefüggések.
Vegyük tehát sorra:
generikus, unit, flatMap¶
- Egy monád: egy
M[T]
generikus osztály. - Kell legyen egy olyan ún.
unit[T]: T => M[T]
generikus metódus/függvény, mely egyT
értékből egyM[T]
értéket készít. Ez Scalában jó közelítéssel azM
osztály companion objectjében egyapply(value: T): M[T]
metódus. - Kell legyen egy
flatMap[U]( f: T=>M[U] ): M[U]
metódusa. - Monád axiómák: mindjárt rátérünk.
Eddig a List
-re ezek teljesülnek:
List[T]
tényleg generikus osztály,T
típusú elemeket tároló listák az ebbe a típusba tartozó értékek.- Van neki
unit
függvénye, hiszen pl. aList(3)
hívással a3
-ból, ami egyInt
, készítünk egyList[Int]
-et, mégpedig az egyelemű(3)
listát.Int
-bőlList[Int]
-et, általában megT
-bőlList[T]
-t, eddig rendben van. - Van neki
flatMap
metódusa, pont ezzel a szignatúrával:flatMap[U]( f: T=>List[U] ): List[U]
.
és a monád axiómák¶
Három monád axióma van, aminek minden x: T
, f:T=>M[U]
, g:U=>M[V]
és m:M[T]
típusú értékre teljesülnie kell,
hogy M
-et monádnak nevezzük:
unit(x).flatMap(f) = f(x)
- ez a ,,balegység'' tulajdonság,m.flatMap(unit) = m
- ez a ,,jobbegység'' tulajdonság,m.flatMap(f).flatMap(g) = m.flatMap( x => f(x).flatMap(g) )
- ez az ,,asszociativitás''.
a List
osztály teljesíti az axiómákat, tehát monád¶
Hogy kicsit könnyebben emészthető legyen, nézzük meg, ezek mit is jelentenek konkrétan az M = List
osztály esetén:
- balegység:
List(x).flatMap(f) = f(x)
kéne teljesüljök, haf:T=>List[U]
függvény. Nézzük, mi is ez:f(x)
ezek szerint valami lista,U
típusú elemeké, legyen mondjuk(y1,y2,..,yk)
. Ez van a jobb oldalon.List(x)
az egy egyelemű lista,x
van benne egyedül.- ha ezen az egyelemű listán flatmappelünk, mondjuk úgy nézzük, hogy előbb mapelünk, aztán flattenelünk:
- map után
(x)
-ből lesz((y1,y2,..,yk))
(note a dupla zárójelet: ez most egyList[List[U]]
) - flatten után ebből is
(y1,y2,...,yk)
lesz, tehát a két oldal tényleg egyenlő.
- jobbegység:
m.flatMap( List.apply ) = m
, ham
egy tetszőleges lista. Nézzük:- legyen mondjuk
m=(x1,x2,...,xn)
egy lista - ezen alkalmazva a
flatMap
-et aList.apply
metódussal (ami aunit
Scala megfelelője), végig kell mennünk mondjuk a listán, mapni minden elemét aList(_)
metódussal, aztán flattenelni az eredményt - ha ezzel a metódussal mapünk, akkor abból először is lesz
((x1),(x2),...,(xn))
(minden elem helyébe bekerül az őt tartalmazó egyelemű lista) - aztán flattenelés után ebből visszakapjuk a
(x1,x2,..,xn)
listát, tehát a két oldal tényleg egyenlő.
- legyen mondjuk
- asszociativitás:
m.flatMap(f).flatMap(g) = m.flatMap( x => f(x).flatMap(g) )
, ham
egy lista,f
ésg
meg megfelelő szignatúrájú függvények. Lássuk, mi ez:- legyen
m=(x1,x2,...,xn)
a listánk - hogy beszélni tudjunk az
f
-ről, ennek azx
-ekhez egy-egy (mondjuk)U
típusú listát kell visszaadnia, legyen mondjukf(x1) = (y11,y12,..,y1t1)
,f(x2)=(y21,y22,...,y2t2)
stb. - akkor
m.flatMap(f) = (y11,y12,...,y1t1,y21,y22,...,y2t2,...,yn1,yn2,..,yntn)
, az összes eredménylista konkatenáltja - a
g
függvény ezekből azy
-okból mindből készít egy-egy (mondjuk)V
típusú listát, legyen mondjukg(yij) = (zij1,zij2,..,zijkij)
- a hosszú
y
listánkon pedig ezzel flatmapve azt kapjuk, hogym.flatMap(f).flatMap(g) = (z111,z112,...,z11k11,z121,z122,...,z12k12,...,zntn1,zntn2,...,znznknzn)
, a lényeg, hogy itt meg ezek a listák vannak megfelelő sorrendben egymás után konkatenálva: legelőször mindenki, aki végülisx1
-ből készült, majd azok, akik végülisx2
-ből, stb. - lássuk a jobb oldalt:
m.flatMap( x => f(x).flatMap(g) )
, nézzük, akkor mindenxi
-n ezt a belső függvényt,f(xi).flatMap(g)
-t kell kiértékelnünk és az eredménylistákat egymás után fűzni - ebből
f(xi)=(yi1,..,yiti)
- és ha ezen flatmapjük
g
-t, azt kapjuk, hogyxi
képe(zi11,zi12,..,zi1ni1,zi21,zi22,...,zi2ni2,...,ziti1,...,zitiniti)
- amiket ha egymás után írunk az
xi
-k eredeti sorrendjében, itt is visszakapjuk az összes olyan elem listáját ugyanabban a sorrendben, mint az előbb, amiket úgy kapunk, hogy az eredeti lista elemein alkalmazzukf
-et, az így előálló listák elemeinek mindegyikén alkalmazzukg
-t, és az így előálló listák elemeit elsősorban azx
indexe, másodsorban ha azx
megegyezik, akkor azf(x)
-beli lista sorrendje szerint rakjuk őket sorba.
- legyen
Tehát a List
egy monád.
monádban van map és flatten is¶
Kicsit korábban kifejezgettük oda-vissza a flatMap
, flatten
és map
metódusokkal (és a List.apply
metódussal) egymást. Ez nem volt véletlen:
ha van egy monádunk, abban pontosan úgy definiáljuk a map és flatten metódusokat, mint ahogy a listában tettük.
Persze a Scala nem fogja nekünk ellenőrizni, hogy ha van egy generikus osztályunk, flatMap
pel és apply[T]
vel a companion objectben, akkor
vajon teljesítjük-e a monád axiómákat és ha van még map
meg flatten
metódusunk is, annak valóban ,,köze van-e'' a flatMap
hez. Mindazonáltal
jó gyakorlat az, ha így nevezzük el a metódusainkat, akkor így is viselkedjenek; persze lehet hatékonyabban implementálni őket, mint a lenti ,,definícióban''
ahogy szerepel, a lényeg, hogy ugyanazt az értéket adja, mint amit a két lenti összefüggés megszab:
- Ha
M
egy monád, akkor amap
metódusát definiáljuk a következőképpen: ham:M[T]
ésf:T=>U
, akkor legyen1
m.map(f) = m.flatMap( x => unit(f(x)) )
- Ha
M
egy monád, akkor aflatten
metódusát definiáljuk a következőképpen: ham:M[M[T]]
, akkor legyen1
m.flatten = m.flatMap( x=>x )
Ellenőrizzük le, hogy ezek az absztrakt monád metódus definíciók megfelelnek a List
monád azonos nevű metódusainak!
Válasz mutatása
Tehát mostantól ha monádokról beszélünk, azokban lesz flatMap és unit, és az így definiált map és flatten is.
Persze a List
-nek is, mint minden beépített osztálynak, ha van egy adott célra megnevezett függvénye, akkor használjuk is azt, nehogy pl. egy mapet úgy
végezzünk el, hogy egyelemű listákat építünk, aztán flatMapelünk. A lényeg csak az, hogy egy monádban a map
, flatten
, flatMap
és unit
metódusok
részéről elvárt egyfajta ,,kontrakt'', hogy úgy viselkedjenek, hogy megtehessünk ezen az alapon valamiféle átírásokat kiértékeléskor, és tudjunk bizonyítani
a programunkról (jó esetben legalább félig automatikusan) helyességet.