Kompozice monád & Option

Nedávno jsem ve Scale psal skript pro zpracování obrázků postahovaných z 4chanu. V jednom z několika kroků jsem kalkuloval hash obrázku, který mohl být vypočítán jen pro některé soubory (na detaily se neptejte). Typický javovský přístup je vracet string pro soubory, které lze hashovat a null pro ty méně šťastné. Zpracování by pak vypadalo nějak takto:
for { f <- dir.listFiles h = hash(f) // String nebo null if (h != null) } yield (f, h) // (File, String)
Všechno funguje, ale nešlo by to lépe? Konkrétně se nějak zbavit té podmínky.
Kompozice monád by to vyřešila. Stačí, aby funkce hash
místo obyčejného stringu
vracela Option[String]
a kód se rázem zjednoduší:
for { f <- dir.listFiles // monáda h <- hash(f) // monáda } yield (f, h) // tadá (File, String)
Podmínka zmizela, ale výsledek je stejný. Co to sakra?
Trik je v tom, že Option je (stejně jako například všechny kolekce
Scaly) monáda a ty se dají řetězit a komponovat (monadické
rovnice si dovolím odpustit). Dále: místo přiřazení byl použit
generátor (<-
) a dva po sobě joucí generátory se přepíšou na volání metody
map
následované flatMap a právě flatMap
zahladí všechny stopy.
Option má oproti null jenom samé výhody:
- je bezpečnější: Když funkce vrací Option, oznamuje, že může vrátit hodnotu, která nedává smysl a klient s tím musí počítat. Takto se dá snadno předejít
NullPoiterException
.
hash(file) match { case None => println("tak nic") case Option(hashString) => println("hash souboru "+file+" je "+hashString) }
- oproti null má jasně definovanou sémantiku: když funkce vrátí null, co to vastně znamená? Jde o chybu nebo správný výsledek? Option je v tomhle ohledu jasnější: None naznačuje, že se nevrátilo nic zajímavého, Some validní výsledek (a může to být i Some(null))
val s = List("Adam K.", null, "Peo", "Ruby") s.headOption // Some("Adam K.") s.tail.headOption // Some(null) List().headOption // None
Pokud Option nepoužíváme v konstrukci for
, můžeme s mím pracovat následujícím způsobem:
(opt // opt je typu Option[X] .map(_.toString) // Option[String] .getOrElse("nothing") // String )
Všimněte si, že nikde nehrozí riziko NullPoiterException
.
A to nejlepší na konec: ke zkonvertování jakékoli hodnoty na Option monádu, stačí tuto hodnotu jednoduše do Option zabalit.
val s: String = null val opt = Option(s) // None val s2: String = "47" val opt2 = Option(s2) // Option[String]
Zkrátka Option by se mělo používat všude tam, kde by se v Javě použil null.