25 - az Option monád¶
Az Option monád opcionális értékek tárolására alkalmas, melyek esetleg hiányozhatnak.
a probléma felvetése, Javában¶
Lássuk, mi is az ,,opcionális'' értékek kezelésének bevett módja Javában (bár amióta Javában is megjelentek
a funkcionális paradigma elemei, ott is létezik az Optional<T>
osztály). A következő állatorvosi feladat
alapját Gercsó Márk egyik Javás gyakorló feladata képezi:
Van egy Mazsola
osztályunk, annek egy int meret
adattagja.
Írjunk egy Java függvényt, mely inputként kap egy Mazsola[]
tömböt, melyben lehetnek null
ok is,
adja vissza a legnagyobb mazsolát, vagy null
t, ha a tömb összes eleme null
!
Próbáljunk erre Javában egy implementációt készíteni.
1 2 3 4 5 6 7 8 |
|
Válasz mutatása
Látható, hogy a folyamatos null
checkek sokkal kevésbé olvashatóvá teszik a kódot, és persze
a hibalehetőség is nagyobb. Plusz, ha csak annyit lát, aki meghívja ezt a függvényt, és nem
olvassa el a(z esetleg nem is létező) dokumentációt, az lehet, hogy nem számít arra, hogy
null
is visszajöhet, nem látszik a függvény fejlécéből, sem pl az, hogy működnie kéne-e
akkor, ha a tömbben lehetnek null
értékek is.
Számos módon meg lehet ezt a ,,esetleg nem létezik az érték'' problémát, a funkcionális paradigmában
erre az Option
monádot idiomatikus használni.
az Option monád¶
Tehát, ez is egy monád, azaz generikus típus: Option[T]
.
- Egy
Option[T]
az vagySome(value: T)
, vagyNone
(utóbbi, hasonlóan az üresList
hez, object, ami mindenOption[T]
-nek példánya - ,,nullable'' értékek helyett ezt használjuk
- a
map
,flatMap
,filter
foreach
,fold
,isEmpty
,size
metódusok pontosan úgy viselkednek, mintha vagy egy üres, vagy egy egyelemű listával lenne dolgunk - mivel van
flatMap
je ésforeach
e is, lehet tenni for comprehensionbe - lehet rá pattern matchelni, de inkább a for comprehension az idiomatikus megoldás
get
metódus: ha nemüres, akkor visszaadja avalue
mezőt, ha üres, kivételt dobgetOrElse(default: =>T): T
metódus: ha nemüres, akkor visszaadja az értéket, ha üres, akkor adefault
értéket adja (figyeljük meg: ez név szerinti paraméterátadás! ha bonyolult kifejezést adunk, az csak akkor kerül kiértékelésre, haNone
az optionünk
példák¶
A lenti kódok eredményeit átgondolhatjuk azon az alapon is, hogy mi történne egy nulla-vagy-egy elemű listával a megfelelő műveletek alkalmazása során:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
az osztály kezdetleges megvalósítása¶
Implementáljunk egy saját ilyen osztályt (egyelőre azzal, hogy az üres opció is lehet case class case object helyett)! Először még csak az Option[T]
traitet
a flatMap
, map
, filter
, foreach
, isEmpty
, size
metódusokkal. Meg tudjuk valósítani valamelyiket már a traitben?
1 2 3 4 5 6 7 8 9 |
|
Válasz mutatása
Most implementáljuk a None
osztályt! Egyelőre legyen case class.
1 2 3 4 5 6 7 8 |
|
Válasz mutatása
Implementáljuk a Some[T]
osztályt is!
1 2 3 4 5 6 7 8 9 |
|
Válasz mutatása
a mazsolás feladat Optionnal¶
Mit kapjon akkor a megvalósítandó függvényünk, ha a mazsolás feladatot idiomatikus Scalában valósítjuk meg?
Válasz mutatása
Mit adjon vissza?
Válasz mutatása
Mondjuk kezdetben a bejövö Vector[Option[Mazsola]]
-ból szeretnénk készíteni egy Vector[Mazsola]
-t, ami csak a tényleges értékeket (magukat a mazsolákat)
tartalmazza.
Ez első megközelítésben megoldható például egy két enumerátoros for yield comprehensionnel. Hogyan?
1 2 3 4 5 6 7 8 |
|
Válasz mutatása
Itt tehát azt látjuk, hogy egy Vector
enumerátoron belülre egy Option
enumerátor rakása nem okoz gondot: a fenti kódból
gondoljuk át, milyen kifejezést készít a Scala fordító?
1 |
|
Válasz mutatása
Tudjuk ezt a fenti kódot egyszerűsíteni?
1 |
|
1 |
|
Válasz mutatása
Hogy ez az egymásbaágyazott enumerátor működik, annak megint az az egyik oka, hogy a Vector
is és az Option
is IterableOnce
-ok.
Amit tanultunk ebből a kódból:
- ha
Option[T]
-k egyc
kollekciójábol szeretnénk egy ugyanolyan kollekciót készíteni, de már aT
underlying típussal, azokból az értékekből, melyek léteznek ac
kollekcióban, akkor csak egyflatten
t kell hívnunk.
Most már, hogy van egy Vector[Mazsola]
nk, ebből kell a legnagyobbat kiválasztani. Erre is van több lehetőségünk.
Melyik volt az a listaművelet, amelyik erre alkalmasnak tűnhet?
Válasz mutatása
A kettő közül most talán célravezetőbb lehet egy üresség check után reduceolni. Hogyan?
1 2 |
|
Válasz mutatása
Mivel most az aggregált érték szintúgy mazsola lesz, mint ami a vektorban van, lehet használnunk a reduce
függvényt, ami két mazsolából
számolja ki az ,,aggregált'' mazsolát -- maximumot keresünk egy függvény szerint, tehát azt választjuk, amelyik a nagyobb. Ez rendben is
lesz, nem baj, ha a sorrend nemdeterminisztikus, hiszen a maximum függvény asszociatív.
Scalában a tervezési elv az, hogy szinte minden gyakran használt use casere van függvény implementálva, így ilyenkor ha keresünk, jó eséllyel találunk is olyat, mellyel a kódunk kompaktabb lesz. Alapvetően ha a típus összehasonlítható, akkor pl.
- a
max
függvény visszaadja a kollekcióban lévő legnagyobb elemet, vagy kivételt dob, ha a kollekció üres, - a
maxOption
függvény visszaadja egySome
ban a kollekció legnagyobb elemét, ha az nemüres, ésNone
-t ad, ha üres, - a
maxBy( f: T=>B ): T
függvényt akkor használhatjuk, ha vagy alapból nem összehasonlítható a kollekciónk alaptípusa, vagy nem aszerint a rendezés szerint akarunk maximumot keresni: ilyenkor megadhatunk egy függvényt, és azt az elemet kapjuk vissza, akin ez a megadottf
függvény a legnagyobb értéket veszi fel (a kimenetiB
típus rendezhető kell legyen, annak a rendezése szerinti maximumot keres), vagy kivételt dob, ha üres a kollekció, - a
maxByOption( f: T=>B ): Option[T]
pedigNone
-t ad, ha üres a kollekció ésSome(value)
-t, havalue
az az eleme a kollekciónak, melyref(value)
a maximális.
A fentiek közül akkor melyiket használva tudunk olvashatóbb kódot írni, hogy kész legyen a legnagyobb
függvényünk?
1 |
|
Válasz mutatása
Olvasható azoknak, akik láttak már Scalát, és maga a fejlesztő is meg lehet róla győződve, hogy nem követett el hibát, ha egy ilyen függvényt implementál. (Azért teszteljük le, lássuk, hogy ez tényleg fordul és működik.) Kicsit nagyobb rutinnal a kollekciók és az opciók terén persze nem kell végigjárni mindig ezt a fejlesztői kört, gyorsan tudunk írni hasonlóan kompakt, olvasható kódot.
egy nem forduló for yield¶
Mi van, ha fordított a helyzet monád egymásbaágyazás terén és nem egy Vector[Option[T]]
-vel, hanem egy Option[Vector[T]]
-vel kell dolgoznunk?
Gondoljuk át, mi és miért történik, ha egymásba ágyazott enumerátorokkal dolgozzuk fel.
A foreach nem gond:
1 2 3 |
|
foreach
eleve Unit
ot ad vissza, és úgyis eldobjuk, itt jellemzően fordulni fog bármi, mindegy, milyen sorrendben tesszük
(persze a sorrend attól azért függ, hogy milyen sorrendben akarunk mellékhatni, pl printelni valamit), de a flatmappel már más a helyzet:
1 2 3 |
|
1 |
|
elem => elem
identikus függvény melletti mapet
, ami fölösleges, minek írjuk át ugyanarra a vectort, ami marad:
1 |
|
flatMap
van identikus függvénnyel, erre mondtuk, hogy flatten
, ha a belső és a külső monád ugyanaz, de most nem az:
most van egy Option[Vector[String]]
-ünk, amin az eddigi flatten
alapján úgy kéne hatnunk, hogy a belső monádot megszüntetjük.
Kapnunk kéne egy Option[String]
-et - de hogyan? Milyen alapon készítenénk egy stringet (well, vagy nullát) abból a string vektorból?
Persze, hogy nincs rá automatizmus, és ezért rekamál a fordító, hogy ahol ő egy Vector[String]
et lát, ott csak egy Option[String]
-gel
tudna mit kezdeni.
Ezek alapján vajon miből mit lehet konvertálni List
, Vector
és Ǫption
közt metódussal?