funkcionálně.cz

Přední český blog o funkcionálním programování, kde se o funkcionálním programování nepíše
««« »»»

Jak vlastně psát asynchronní kód

13. 8. 2013 — k47

V ná­vaz­nosti na React.PHP před­nášku se mě ně­ko­lik lidí ne­zá­visle na sobě ptalo na jednu otázku: jak vlastně psát asyn­chronní kód.

(ná­sle­duje pře­lo­žená a dra­ma­ti­zo­vaná verze části mailu od @ju­znacz)

Když po­u­ží­vám vlákna, všechny ope­race můžou blo­ko­vat. Při­dání blo­ku­jí­cího IO je v ta­ko­vém pří­padě velice jed­no­du­ché. Stačí jenom napsat pár řádků kódu na po­ža­do­vané místo. Z hla­vičky metody není zřejmé, jestli metoda sama (nebo uvnitř volané metody) pro­vádí nějaké IO.

Na­proti tomu v asyn­chron­ním světě, kde vládne pro­mise (nebo future, jak se jme­nuje v jiných ja­zy­cích), ně­které metody ne­dě­lají žádné IO (ty vra­cejí pros­tou hod­notu) a ně­které jsou asyn­chronní (a vra­cejí promise). Promise jsou z pod­staty na­kaž­livé – pokud a() vrací promise, všechny metody a funkce, které volají a() musí také vracet promise. To může zkom­pli­ko­vat re­fak­to­ring. Které metody tedy mají vracet promise (jinými slovy jsou nebo můžou být asyn­chronní) a které by měly vracet prosté hod­noty?

Nebylo by lepší vždycky vracet promise, i když budou ve vět­šině pří­padů re­sol­ved (tedy už mající hod­notu). To by po­mohlo re­fak­to­ringu.

Také k otázce roz­ši­ři­tel­nosti: co když zá­kladní im­ple­men­tace (např. UI\Presenter::formatTemplateFile()) nedělá žádné IO a vrací pros­tou hod­notu, ale po­tře­buji ta­ko­vou třídu roz­ší­řit a pře­psané metody už nějaké to IO pro­vá­dějí. Pak budou muset vracet promise.

Od­po­věď je celkem jed­no­du­chá, ale než k ní dojdu, musím se dostat až do sa­mot­ného srdce funk­ci­o­nál­ního pro­gra­mo­vání a zlomit pro­kletí monád.

Pro­mise je totiž monáda1 a kód, který s pro­mise pra­cuje, je už z pod­staty velice funk­ci­o­nální: místo mutací po­u­žívá trans­for­mace (vý­sle­dek jedné ope­race je předán jako ar­gu­ment té ná­sle­du­jící) a všechny (ve­d­lejší) efekty (v tomto pří­padě pro­vá­dění asyn­chron­ního IO) jsou při­znané. Z toho pak vy­plývá fakt, že se ze sig­na­tury metody dá poznat, jestli má nějaké efekty nebo nemá a to je jednak nutné (z pro­mise nemůžu vy­stou­pit), ale také uži­tečné, pro­tože mi to říká něco o cho­vání té které metody.

Pro­mise, stejně jako všechny monády, jsou na­kaž­livé a je pro to dobrý důvod. Vy­tvá­řejí totiž určitý kon­text (v našem pří­padě IO) vý­po­čtu, skrz který je všechno sek­ven­co­vané (metoda then v jistém slova smyslu fun­guje jako střed­ník, pro­tože určuje v jakém pořadí se bude pro­gram pro­vá­dět – $a(); $b() v syn­chron­ním světě od­po­vídá $a->then($b) v tom asyn­chron­ním). Kdy­bych z pro­mise mohl utéct ven, zna­me­nalo by to, že můžu pod­vá­dět a za­ta­jo­vat ně­které efekty a na­bou­rat celou abs­trakci (unik­nout se dá jenom z ko-monád, ale ty si nechám na jindy).

Kdyby pro­mise měla hy­po­te­tic­kou metodu value() vra­ce­jící ak­tu­ální hod­notu uvnitř pro­mise. Jak by byla taková metoda im­ple­me­to­vaná? Když hod­nota exis­tuje, jed­no­duše jí vrátí, když ob­sa­huje chybu, vyhodí vý­jimku a co když pro­mise zatím žádnou hod­notu nemá? Má blo­ko­vat? Má blo­ko­vat s ti­me­ou­tem? Má vy­ho­dit vý­jimku? To není vůbec jasné. Na­jed­nou se na­chá­zím v jiném světě, který se řídí jinými pra­vi­dly. Ne na­darmo se můžu na mo­na­dický kód dívat jako na abs­traktní pro­gram, který je danou mo­ná­dou in­ter­pre­to­ván.

Jak tedy na to?

Ře­še­ním je psát co nej­větší část kódu v duchu funk­ci­o­nál­ního pro­gra­mo­vání, kdy funkce a metody jenom trans­for­mují hod­noty (values), pro­vá­dějí vý­po­čet, ale ne­re­a­li­zují žádné (ve­d­lejší) efekty a kolem tohoto jádra tak­zva­ných pure funkcí po­sta­vit sko­řápku, která není pure, ko­mu­ni­kuje s okolím a dělá IO. Tím pádem vnitřek není na­ka­žen virem pro­mise, jde o běžný kód se kterým můžu pra­co­vat, jak jsem zvyklý. V ide­ál­ním pří­padě by mělo čiré jádro být co nej­větší a ne­čistá vrstva vy­tla­čena na okraj pro­gramu, místo toho aby byla pro­tkána celým sys­té­mem, což značně usnadní jeho po­cho­pení.

To zá­ro­veň od­po­vídá na otázku, zdali by se ne­vy­pla­tilo ze všech metod vracet pro­mise. Není to po­třeba a ani se to ne­vy­platí. Není to třeba, pro­tože systém má tyto dvě vrstvy (čirá/nečirá), které jsou zcela od­dě­lené. Když od nějaké metody čekám, že může dělat async IO, pak musí vracet pro­mise. Kdyby se stalo to, že předek vrací hod­notu a po­to­mek pro­mise, pak to po­ru­šuje Liskov sub­sti­tu­tion prin­ci­ple na celkem zá­sadní úrovni.

Ne­vy­platí se to, pro­tože čiré funkce jsou re­fe­renčně transpa­rentní (což je ta hlavní myš­lenka FP). Když funkce nemá ve­d­lejší efekty a její výstup je zá­vislý jenom na jejích ar­gu­men­tech, má na­jed­nou ně­ko­lik za­jí­ma­vých vlast­ností: můžu ji volat ko­li­krát chci a vždycky do­stanu stejný vý­sle­dek (je tedy idem­po­tentní), ne­mu­sím se bát, že každá in­vokvace nějak změní okolní svět, její vý­sle­dek můžu li­bo­volně ke­šo­vat a sdílet. Čiré funkce a ne­měnné hod­noty samy o sobě do­ká­žou zna­telně zjed­no­du­šit vý­sledný kód a mají mnoho dal­ších před­ností.


Pozn:

  1. I když spe­ci­fi­kace Pro­mise/A ne­spl­ňuje roz­hraní monád (funkce bind, return), tak ji přesto můžu po­va­žo­vat za jednu z nich.
@kaja47, kaja47@k47.cz, deadbeef.k47.cz, starší články