33 - a Try monád¶
Kivételkezelésről nem volt még szó, pedig pl. Javában bevett szokás kivételek dobálásával alakítani a vezérlést, például:
- ha egy metódus olyan argumentumot kap, ami nem felel meg a specifikációnak, szokás dobni egy
IllegalArgumentException
t - ha egy file írásakor/olvasásakor vannak gondok, szokás dobni valamiféle
IOException
t, példáulFileNotFoundException
t - ha a program valamiért inkonzisztens állapotba kerül (ez inkább programozási logikai hibára utal,
mintsem felhasználói oldali hibára), szokás dobni
IllegalStateException
t - ha egy tömböt túlcímzünk, szokás dobni
ArrayIndexOutOfBoundsException
t - ha nullával egészosztunk, érkezik egy
ArithmeticException
- ha nem szám alakú stringet próbálunk számmá alakítani, érkezik egy
NumberFormatException
- ha egy üres lista
head
jét akarjuk lekérdezni, érkezik egyNoSuchElementException
stb.
Ebben a leckében megnézzük, hogy Scalában ezt hogyan kezeljük.
Mivel a Scalás beépített kivételosztályok hierarchiája nagy részben megegyezik a Javással (számos kivétel a Scalában egy az egyben Javából van importolva), érdemes lehet feleleveníteni a progegyen tanultakat legalább koncepcionálisan a lecke megkezdése előtt.
Option on steroids¶
Kivételek kezelésére a Try
monád használata egy Scala idiomatikus módszer, hasonlít az Option
-re.
Egy Option[T]
:
- vagy egy
Some( value: T )
, ami tárol egyT
típusú értéket, - vagy a
None
objektum, ami ,,üres doboz'',
és ahelyett, hogy egy optionra matchelnénk, a map
, flatMap
, foreach
stb. függvényekkel
(akár for comprehensionben enumerálva) dolgozunk vele, és csak egy logikai folyamat legvégén
,,nézünk bele'' a dobozba.
Ehhez képest egy scala.util.Try[T]
:
- vagy egy
Success( value: T)
, ami tárol egyT
típusú értéket, - vagy egy
Failure( problem: Throwable )
, ami tárol egy Throwablét (tehát jellemzően egy kivételt pl. az intróban felsoroltak közül).
Ha egy olyan T
típusú e
kifejezést kell kiértékelnünk, mely kiértékelés közben kivételt dobhat,
eljárhatunk pl. így:
1 2 3 |
|
Ekkor
- ha
e
kiértékelése sikerült, az érték mondjukvalue
, akkortryE
értékeSuccess( value )
lesz, - ha
e
kiértékelés közben dobta aproblem
kivételt, akkortryE
értékeFailure( problem )
lesz.
Például ha mondjuk egy számológépet implementálunk, és írunk bele kifejezés-kiértékelőt, van esélye, hogy nullával osszunk, amikor is elszáll a program:
Ugyanakkor ha a kifejezés kiértékelését egy Try
dobozban végezzük, nincs probléma:
Láthatjuk, hogy nem piros a konzol, a program szépen lefutott, kiírta a Failure
case classt, benne a kivétellel, de szépen ment tovább a futás.
Hogyan implementálnánk magunk eddig ezt a Try
osztályt?
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Válasz mutatása
Valójában a Try
,,doboz'' minden ún. NonFatal
kivételt és errort elkap, ha megnézzük a NonFatal
objektum
forrását,
azt láthatjuk, hogy ami a Throwable
k közül Fatal
és nem fogja a Try
sem elkapni, azok valóban elég
nasty problémák, többek közt
VirtualMachineError
ThreadDeath
InterruptedException
LinkageError
amikkel ha még nem is találkoztunk, nevük alapján valóban olyasminek hangzanak, amik ha felbukkannak valahol, ott a programot futtatni próbálni valami konzisztens állapotban tovább nincs értelme.
foreach, map, flatMap¶
A Try
monadikus metódusai nagyon hasonlóak az Option
éhoz abban a tekintetben, hogy ha Success
az objektumunk, akkor a belsejében lévő objektumon értékelünk ki egy függvényt, ha pedig Failure
,
akkor nem történik semmi, visszakapjuk a failúránkat változatlanul. Tehát:
foreach¶
A Try
osztály foreach( f: T=>U ): Unit
metódusa
- ha ez egy
Success( value )
, akkor kiértékelif(value)
-t (vélhetően mellékhatás kedvéért), - ha ez egy
Failure( problem )
, akkor semmi nem történik.
Tehát például ha e
valamiféle aritmetikai kifejezés, akkor azt hogy érhetjük el, hogy ha kiértékelhető
egy számra, akkor írjuk ki azt a számot, ha meg nem, akkor ne írjunk ki semmit?
1 2 3 4 5 6 7 8 9 10 11 |
|
Válasz mutatása
map¶
A Try
osztály map[U]( f: T=>U ): Try[U]
metódusa
- ha ez egy
Success( value )
, akkor az értékTry( f(value) )
lesz; - ha ez egy
Failure( problem )
, akkor pedugFailure[U]( problem )
lesz.
Mi történik akkor, ha ugyan eredetileg Success
a dobozunk, de az f(value)
kiértékelése kivételt dob?
Válasz mutatása
flatMap¶
Persze olyan is lehet, hogy olyan függvényt akarunk kiértékelni, mely maga is valamiféle Try[U]
-t ad vissza,
pontosan ekkor használható a Try
osztály flatMap[U]( f: T=>Try[U] ): Try[U]
metódusa:
- ha ez egy
Success( value )
, akkor az eredményf(value)
lesz; - ha ez egy
Failure( problem )
, akkor pedigFailure[U]( problem )
lesz.
Ezt megint felfoghatjuk úgy, mintha a Try[ Try[ U ] ]
doboznak a ,,külső rétege'' megszűnne map
elés után,
mikor egy Success
t flatmapelünk.
flatMap
elünk.
filter, getOrElse¶
A filter( p: T=>Boolean ): Try[T]
viselkedése már az Option
alapján nem teljesen egyértelmű: az az eset
nem világos, hogy mi is kéne legyen az érték, ha egy Success( value )
-t filterezünk olyankor, mikor
p(value) == false
?
- ha ez egy
Success( value )
, melyrep(value) == true
, akkor visszakapjuk az eredetiSuccess( value )
-t, - ha ez egy
Success( value )
, melyrep(value) == false
, akkor egyFailure( problem )
-et kapunk, aholproblem
egyNoSuchElementException
. - ha ez egy
Failure( problem )
, akkor visszakapjuk az eredetiFailure( problem )
-et.
A getOrElse( default: => T): T
metódus ismét hasonló az Option
esetéhez:
- ha ez egy
Success( value )
, akkor megkapjukvalue
-t, - ha ez egy
Failure( problem )
, akkor pedig adefault
értéket kapjuk meg.
Hogyan valósítánk meg a következő függvényt: kapjunk meg egy Int
re kiértékelődő kifejezést,
és adjuk vissza a kifejezés értékét String
gé konvertálva, ha ki lehet értékelni, és a "HIBA TÖRTÉNT"
stringet, ha kivételt dob közben?
1 2 3 4 |
|
Válasz mutatása
monád?¶
A Try[T]
-re próbáljuk meg ellenőrizzük a monád axiómákat!
A unit
művelet (mivel annak most T
-ből
kell képeznie Try[T]
-be) a Success
lesz.
Válasz mutatása
use case¶
Általában, ahelyett, ahogy Javában egy metódusnál jelöljük, hogy T func() throws XY
milyen kivételeket dobhat, Scalában
idiomatikusabb a metódus visszatérési értékét T
helyett Try[T]
-re deklarálni: def func(): Try[T]
. Ekkor
- a hívó oldalon az értéket egy
Try[T]
-ben visszük magunkkal, - ha tennénk valamit ezzel az értékkel, amennyiben tényleg egy
T
jött vissza, úgy azt a monadikus metódusokkal:map
,flatMap
,foreach
stb, ha tehetjük, az átláthatóság végett enumerátorokkal tesszük, - így ami Javában
finally
ágba kerülne, azt ugyanúgy ki tudjuk értékelni attól függetlenül, hogy aTry
belsejében éppen egy sikerérték van, vagy egy failúra, - tehát végeredményben mehet egy lineáris programlogika, catch és finally klózok nélkül, egészen addig, amíg a vezérlés
el nem jut egy olyan pontra, ahol a kivételt, ha az jött, recoverelni van értelme, onnan pedig már
Try
nélkül, ténylegesT
értékként tudjuk tovább vinni az eredményt.
recovering¶
Egy módszert a kivétel kezelésére már láttunk: a getOrElse( default: =>T ): T
metódust.
Ezen felül van még több lehetőségünk, ha magával a kivétellel is szeretnénk foglalkozni és annak jellegétől függően más és más recoveryt szeretnénk elvégezni:
Ha például maradnánk Try[T]
-ben, de kivétel esetében egy fix Try[T]
-t szeretnénk kapni, azaz
egy Try( getOrElse( default ) )
-ot hívnánk, ezt megkapjuk másképp is:
- az
orElse( default: => Try[T] ): Try[T]
metódus- ha ez egy
Success( value )
, akkor visszakapjuk változtatás nélkül, - ha pedig
Failure( problem )
, akkor az eredmény adefault
érték lesz.
- ha ez egy
Ha a hibától függő helyreállítást akarunk végezni, arra is több opciónk van:
- a
recover( pf: PartialFunction[Throwable, T] ): Try[T]
metódus:- ha ez egy
Success( value )
, akkor visszakapjuk változtatás nélkül, - ha ez egy
Failure( problem )
, éspf.isDefinedAt(problem)
, akkorSuccess( pf(problem) )
lesz az eredmény, - különben pedig marad `Failure( problem ).
- ha ez egy
Tipikus alkalmazás, amikor is a pf
egy { case nfe: NumberFormatException => ... ; case ioe: IOException => ... }
alakú, mintaillesztéssel definiált parciális függvény, amivel is bizonyos kivételeket helyreállítunk, másokkal meg
nem feltétlen tudunk kezdeni azon a ponton semmit, csak hagyni úgy, ahogy van.
De mi van, ha pf
is dobhat kivételt? Akkor ő egy PartialFunction[ Throwable, Try[T] ]
típusú parciális
függvény és erre másik metódust használhatunk:
- a
recoverWith( pf: PartialFunction[Throwable, Try[T]] ): Try[T]
metódus:- ha a tryunk
Success
, akkor visszakapjuk változtatás nélkül, - ha
Failure( problem )
, depf.isDefined( problem ) == false
(pl nem illeszkedik problemre egyik case se), akkor is visszakapjuk változtatás nélkül, - különben pedig az eredmény
pf( problem )
lesz (ami szintén lehet egy success, helyreállított érték, vagy egy olyan Failure, amit már apf
kiértékelése okozott.
- ha a tryunk
Ha a kivételünket csak el szeretnénk felejteni, akkor:
- a
toOption: Option[T]
metódus- ha a tryunk
Success(value)
, akkor visszakapjukSome(value)
-t, - ha pedig
Failure
, akkorNone
-t.
- ha a tryunk
Amennyiben ezek a lehetőségek nem kínálnak megoldást, van egy általánosabb helyreállító metódus:
- a
transform[U]( s: T => Try[U], f: Throwable => Try[U] ): Try[U]
metódus- ha a tryunk
Success(value)
, akkor az eredménys(value)
, - ha pedig
Failure(problem)
, akkorf(problem)
.
- ha a tryunk
Felmerülhet a kérdés, hogy ha olyan transform
ot szeretnénk alkalmazni, melynek nem Try[U]
a kimenete,
hanem egy U
, akkor mit tegyünk, csak ezért ne csomagoljuk bele egy Success
-be az eredményt, hogy aztán
kibontsuk, nos, az itteni szokatlan fejlécű fold
pont erre a célra való:
- a
fold[U]( fa: Throwable => U, fb: T => U )
metódus- ha a tryunk
Success(value)
, akkor az eredményfb(value)
, - ha pedig
Failure(problem)
, akkorfa(problem)
.
- ha a tryunk
Figyeljük meg, hogy a fold
és a transform
paraméterei pont fordított sorrendben vannak a másikhoz képest..
Végül még említésre méltó ,,hibakezelés'' címen a ,,dobjunk el mindent'' megoldás:
- a
get: T
metódus- ha a tryunk
Success(value)
, akkor az eredményvalue
, - ha pedig
Failure(problem)
, akkorthrow
olja aproblem
et.
- ha a tryunk
Scalán belül ez a legutóbbi ritkán lehet indokolt, azonban ha egy Java oldal felé kiajánlott metódusunk van,
és az ottani konvenciók szerint ,,throws''oló metódust akarunk írni, akkor ez egy megoldás lehet:
kiértékelünk egy kifejezést, amíg végig Scala oldalon vagyunk, addig dobozban tartjuk és a monadikus
műveletekkel kezeljük a tartalmát, majd mikor a Java oldalra kell adjuk az eredményt, ahol nem értik
a dobozainkat, akkor utolsó belerúgásként egy get
tel hozzájuk vágjuk vagy a kivételünket,
vagy az értéket, ha sikeres volt
a számítás vagy helyre tudtuk meaningfully állítani.
Ha még ez is kevés, akkor lehet persze szétszedni a dobozunkat: matchelni a Try
-ra,
Success
esetében kiértékelni valamit, Failure
esetben valami mást, de erre azért kifejezetten ritkán lehet
valid módon szükség.