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

V návaznosti na React.PHP přednášku se mě několik lidí nezávisle na sobě ptalo na jednu otázku: jak vlastně psát asynchronní kód.

(následuje přeložená a dramatizovaná verze části mailu od @juznacz)

Když používám vlákna, všechny operace můžou blokovat. Přidání blokujícího IO je v takovém případě velice jednoduché. Stačí jenom napsat pár řádků kódu na požadované místo. Z hlavičky metody není zřejmé, jestli metoda sama (nebo uvnitř volané metody) provádí nějaké IO.

Naproti tomu v asynchronním světě, kde vládne promise (nebo future, jak se jmenuje v jiných jazycích), některé metody nedělají žádné IO (ty vracejí prostou hodnotu) a některé jsou asynchronní (a vracejí promise). Promise jsou z podstaty nakažlivé - pokud a() vrací promise, všechny metody a funkce, které volají a() musí také vracet promise. To může zkomplikovat refaktoring. Které metody tedy mají vracet promise (jinými slovy jsou nebo můžou být asynchronní) a které by měly vracet prosté hodnoty?

Nebylo by lepší vždycky vracet promise, i když budou ve většině případů resolved (tedy už mající hodnotu). To by pomohlo refaktoringu.

Také k otázce rozšiřitelnosti: co když základní implementace (např. UI\Presenter::formatTemplateFile()) nedělá žádné IO a vrací prostou hodnotu, ale potřebuji takovou třídu rozšířit a přepsané metody už nějaké to IO provádějí. Pak budou muset vracet promise.

Odpověď je celkem jednoduchá, ale než k ní dojdu, musím se dostat až do samotného srdce funkcionálního programování a zlomit prokletí monád.

Promise je totiž monáda1 a kód, který s promise pracuje, je už z podstaty velice funkcionální: místo mutací používá transformace (výsledek jedné operace je předán jako argument té následující) a všechny (vedlejší) efekty (v tomto případě provádění asynchronního IO) jsou přiznané. Z toho pak vyplývá fakt, že se ze signatury metody dá poznat, jestli má nějaké efekty nebo nemá a to je jednak nutné (z promise nemůžu vystoupit), ale také užitečné, protože mi to říká něco o chování té které metody.

Promise, stejně jako všechny monády, jsou nakažlivé a je pro to dobrý důvod. Vytvářejí totiž určitý kontext (v našem případě IO) výpočtu, skrz který je všechno sekvencované (metoda then v jistém slova smyslu funguje jako středník, protože určuje v jakém pořadí se bude program provádět - $a(); $b() v synchronním světě odpovídá $a->then($b) v tom asynchronním). Kdybych z promise mohl utéct ven, znamenalo by to, že můžu podvádět a zatajovat některé efekty a nabourat celou abstrakci (uniknout se dá jenom z ko-monád, ale ty si nechám na jindy).

Kdyby promise měla hypotetickou metodu value() vracející aktuální hodnotu uvnitř promise. Jak by byla taková metoda implemetovaná? Když hodnota existuje, jednoduše jí vrátí, když obsahuje chybu, vyhodí výjimku a co když promise zatím žádnou hodnotu nemá? Má blokovat? Má blokovat s timeoutem? Má vyhodit výjimku? To není vůbec jasné. Najednou se nacházím v jiném světě, který se řídí jinými pravidly. Ne nadarmo se můžu na monadický kód dívat jako na abstraktní program, který je danou monádou interpretován.

Jak tedy na to?

Řešením je psát co největší část kódu v duchu funkcionálního programování, kdy funkce a metody jenom transformují hodnoty (values), provádějí výpočet, ale nerealizují žádné (vedlejší) efekty a kolem tohoto jádra takzvaných pure funkcí postavit skořápku, která není pure, komunikuje s okolím a dělá IO. Tím pádem vnitřek není nakažen virem promise, jde o běžný kód se kterým můžu pracovat, jak jsem zvyklý. V ideálním případě by mělo čiré jádro být co největší a nečistá vrstva vytlačena na okraj programu, místo toho aby byla protkána celým systémem, což značně usnadní jeho pochopení.

To zároveň odpovídá na otázku, zdali by se nevyplatilo ze všech metod vracet promise. Není to potřeba a ani se to nevyplatí. Není to třeba, protože systém má tyto dvě vrstvy (čirá/nečirá), které jsou zcela oddělené. Když od nějaké metody čekám, že může dělat async IO, pak musí vracet promise. Kdyby se stalo to, že předek vrací hodnotu a potomek promise, pak to porušuje Liskov substitution principle na celkem zásadní úrovni.

Nevyplatí se to, protože čiré funkce jsou referenčně transparentní (což je ta hlavní myšlenka FP). Když funkce nemá vedlejší efekty a její výstup je závislý jenom na jejích argumentech, má najednou několik zajímavých vlastností: můžu ji volat kolikrát chci a vždycky dostanu stejný výsledek (je tedy idempotentní), nemusím se bát, že každá invokvace nějak změní okolní svět, její výsledek můžu libovolně kešovat a sdílet. Čiré funkce a neměnné hodnoty samy o sobě dokážou znatelně zjednodušit výsledný kód a mají mnoho dalších předností.


Pozn:

  1. I když specifikace Promise/A nesplňuje rozhraní monád (funkce bind, return), tak ji přesto můžu považovat za jednu z nich.
@kaja47, kaja47@k47.cz, deadbeef.k47.cz, starší články