k47.cz  — každý den dokud se vám to nezačne líbit
výběr foto Praha povídky kultura
TECH ▞▞ kolo | twitter RSS

Kompozice monád & Option

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

Ne­dávno jsem ve Scale psal skript pro zpra­co­vání ob­rázků po­sta­ho­va­ných z 4chanu. V jednom z ně­ko­lika kroků jsem kal­ku­lo­val hash ob­rázku, který mohl být vy­po­čí­tán jen pro ně­které sou­bory (na de­taily se ne­ptejte). Ty­pický ja­vov­ský pří­stup je vracet string pro sou­bory, které lze ha­sho­vat a null pro ty méně šťastné. Zpra­co­vání by pak vy­pa­dalo nějak takto:

for {
  f <- dir.listFiles
  h = hash(f)         // String nebo null
  if (h != null)
} yield (f, h)        // (File, String)

Všechno fun­guje, ale nešlo by to lépe? Kon­krétně se nějak zbavit té pod­mínky.

Kom­po­zice monád by to vy­ře­šila. Stačí, aby funkce hash místo oby­čej­ného stringu vra­cela Option[String] a kód se rázem zjed­no­duší:

for {
  f <- dir.listFiles // monáda
  h <- hash(f)       // monáda
} yield (f, h)       // tadá (File, String)

Pod­mínka zmi­zela, ale vý­sle­dek je stejný. Co to sakra?

Trik je v tom, že Option je (stejně jako na­pří­klad všechny ko­lekce Scaly) monáda a ty se dají ře­tě­zit a kom­po­no­vat (mo­na­dické rov­nice si do­vo­lím od­pus­tit). Dále: místo při­řa­zení byl použit ge­ne­rá­tor (<-) a dva po sobě joucí ge­ne­rá­tory se pře­pí­šou na volání metody map ná­sle­do­vané flatMap a právě flatMap za­hladí všechny stopy.


Option má oproti null jenom samé výhody:

  1. je bez­peč­nější: Když funkce vrací Option, ozna­muje, že může vrátit hod­notu, která nedává smysl a klient s tím musí po­čí­tat. Takto se dá snadno pře­de­jít NullPoiterException.
hash(file) match {
  case None => println("tak nic")
  case Option(hashString) => println("hash souboru "+file+" je "+hashString)
}
  1. oproti null má jasně de­fi­no­va­nou sé­man­tiku: když funkce vrátí null, co to vastně zna­mená? Jde o chybu nebo správný vý­sle­dek? Option je v tomhle ohledu jas­nější: None na­zna­čuje, že se ne­vrá­tilo nic za­jí­ma­vého, Some va­lidní vý­sle­dek (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 ne­po­u­ží­váme v kon­strukci for, můžeme s mím pra­co­vat ná­sle­du­jí­cím způ­so­bem:

(opt                     // opt je typu Option[X]
  .map(_.toString)       // Option[String]
  .getOrElse("nothing")  // String
)

Všim­něte si, že nikde ne­hrozí riziko NullPoiterException.


A to nej­lepší na konec: ke zkon­ver­to­vání ja­ké­koli hod­noty na Option monádu, stačí tuto hod­notu jed­no­duše do Option za­ba­lit.

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 po­u­ží­vat všude tam, kde by se v Javě použil null.

píše k47 & hosté, ascii@k47.cz