k47.cz
mastodon twitter RSS
bandcamp explorer

Kompozice monád & Option

23. 6. 2011 (před 11 lety) — k47 (CC by)

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:

  1. 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)
}
  1. 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.

píše k47, ascii@k47.cz