Dart – Neznesiteľná ľahkosť asynchrónneho bytia

Asynchrónnosť má niečo do seba. Imagine: žiadne thready, žiadne zamykanie objektov, žiadne deadlocks, livelocks. Žiadne webservery s vymrazenými 4 vláknami. Žiadne continuations for rescue (zakričte: fuj) a ďalšie podobné hacky. Žiadne problémy s neefektívne využitými zdrojmi. Kto však píše asynchrónny kód, vie, že táto selanka je len jednou časťou pravdy; asynchrónnosť vie niekedy poriadne skomplikovať život!
Seriál: Úvod do Dartu (9 dílů)
- Dart – Čo? Prečo? 2. 8. 2013
- Dart – Úvod do jazyka 23. 8. 2013
- Dart – Ponorme sa hlbšie 6. 9. 2013
- Dart – v DOMe 19. 9. 2013
- Dart – Futures 4. 10. 2013
- Dart – Streams 17. 10. 2013
- Dart – Používame JavaScript 1. 11. 2013
- Dart Typesystem 19. 11. 2013
- Dart – Neznesiteľná ľahkosť asynchrónneho bytia 2. 12. 2013
Nálepky:
Warmup
Začnime niečím na zahriatie. Definujme si funkciu ajax
import 'dart:async'; //potrebne pre Futures
Future<Map> ajax(Map req) {
//future caka 1 sekundu, potom sa zacne vykonavat
return new Future.delayed(new Duration(seconds: 1)).then(
(_){
//factory konstruktor, vytvori shallow copy of req
Map res = new Map.from(req);
res['data']++;
return res;
});
}
Funckia teda čaká jednu sekundu, potom pochrúme vstup a vráti response objekt (všetko Map-y) obalené vo futures. Názov ajax
má asociovať dlhotrvajúcu operáciu a zároveň jemne pripomína predchádzajúci diel tohto seriálu. Garantujem vám, že pokiaľ takúto rutinu dáte BFC (bežný Franta kóder) a poviete mu, aby spravil tri na seba nadväzujúce ajax-y, napíše niečo takéto:
void main(List<String> args) {
ajax({'data': 0, 'info': 'ahoj svet'}).then(
(res){
ajax(res).then(
(res){
ajax(res).then(
(res){
print(res); //vypise: {data: 3, info: ahoj svet}
});
});
});
}
Samozrejme teda, pokiaľ sa jedná o šikovného BFC, ktorý už do Futures aspoň trochu prenikol. Problémom hore uvedeného kódu je, že je odporný. Vieme ho nejako upratať? Našťasie áno, vďaka peknej vlastnosti – reťazenia futures – sa kód dá prepísať takto (do ukážky som pridal ešte jeden ajax)
ajax({'data': 0, 'info': 'ahoj svet'}).then(
(res) => ajax(res)
).then(
(res) => ajax(res)
).then(
(res) => ajax(res)
).then(
(res){print(res);}
);
Že vám to príde skášlenie v štýle „lepšie ako drótom do oka“? Prepísaním sme sa zbavili kučeravých zátvoriek (táto výhoda sa vytratí v okamihu, ako do then
budete chcieť vložiť hocičo iné ako „oneliner“), oveľa dôležitejšie však je, že výsledný kód nie je príliš vnorený; všetky then-y sa viažu na top-level Future objekt, s ktorým sa postupne reťazia. Keď do dartu pridajú await, bude kód ešte krajší.
Testujeme
Otestujme, či náš ajax
robí to, čo má. Už aj BFCs pochopili, že predtým, ako chceme testovať hodnotu vrátenú z ajax
u používať, treba si na ňu počkať; expect
teda napíšeme do vnútra then
:
void main() {
setUp(){}
test('ajax increments properly', () {
ajax({'data': 0, 'info': 'ahoj svet'}).then(
(res){expect(res['data'], equals(1));}
);
});
}
Náš test krásne funguje a vypíše „PASS: ajax increments properly“. So far so good, čo však keď upravíme expect na expect(res['data'], equals("no bloody way"));
? Náš test stále pass-uje (je to proste kvalitný test, ktorý sa nenechá rozhádzať drobnosťami, akými sú chyby v kóde). V konzole síce uvidíme chybu, ktorú expect spôsobil, test sa však tvári ako bezproblémový.
Skúste sa vcítiť do test runnera (teda, obslužnej rutiny, ktorá sa stará a púšťanie testu). Spustili ste test, test v pokoji dobehol, vytvoril Future a skončil. Načo reportovať chybu? Test runner nemá ako vedieť, že treba čakať na výsledok nejakej Future! Tento fakt mu treba explicitne odkomunikovať použitím expectAsync
test('ajax increments properly v. 2', () {
ajax({'data': 0, 'info': 'ahoj svet'}).then(expectAsync1(
(res){expect(res['data'], "nbw");}
));
});
V tejto verzii už test naozaj funguje (ako má). Poznamenajme ešte, že „1“ na konci expectAsync1
vyjadruje, koľko parametrov bude mať callback, na ktorý sa čaká. Štandardná unittest knižnica ponúka expectAsyncN
pre N=0,1,2, kto chce viac, musí si napísať vlastnú implementáciu (dobrá inšpirácia je tu, dole) Potreba špecifikovať N vzniká z primalej dynamičnosti jazyka, škoda, že Dart nemá niečo ako sú Pythonie splats (o problémoch s typom callbacku ani nehovorím).
Okrem expectAsyncN
existuje protectAsyncN
a guardAsync
, no napriek spamovaniu kóderov v Google nikomu z nás nie je jasné, čo by tieto funckie mali robiť, resp. robia; privítam hociaké rozumné vysvetlenie.
Inou možnosťou, ako test runnerovi oznámiť, že treba počkať na výsledok asynchrónnej operácie je priamo z testu vrátiť Future, runner potom čaká na jej skompletovanie.
Sync Futures
Predstavme si, že chceme nasledovnú funckionalitu:
dynamic processData(String key){
var res;
if(cache.containsKey(key)){ //mame hodnotu v cache?
res = cache[key]; // (*)
} else {
res = ajax(...) // ziskaj hodnotu zo servra
}
doSthWith(res);
return res;
}
Teda, najprv zistíme, či je hodnota v cache, ak nie, vypýtame si ju zo servra. Problémom je, že na konci if
-u nevieme, či v res
sa nachádza priamo hodnota, alebo jej future. Riešenie? Nahradíme riadok (*) týmto:
res = new Future.value(cache[key]);
prípadne
res = new Future.sync(() => cache[key]);
a je to. Na konci if
máme istotu, že res
je Future, v prípade že sme hodnotu vytiahli z cache, je to Future, ktorá completuje hneď v najbližšom event loope (pozor, nie úplne hneď! Toto opozdenie bude asi sotva markantné z hľadiska výkonu, treba však myslieť na to, že aj v tomto prípade sa callback v then
vykoná až po synchrónne nasledujúcich príkazoch).
Okrem vyššie uvedeného use-case sú Future.sync vhodné v prípade, že chcete chytať výnimky v mixe synchrónneho a asynchrónneho kódu, pekná ukážka tu.
Prečo nefunguje try-catch-finally alebo God help us all
Na záver niečo, z čoho sa človeku robí nevoľno: Ak kód skonštruuje Future, ktorá vo svojom then
callbacku vyhodí výnimku, dart VM sa zrúbe, nepomôže try-catch-finally okolo konštrukcie callbacku. Dobre to dokladá nasledovný príklad, kde skonštruovaná delayed future zhodí Timer („here I stand“ sa vypíše cca 10krát, program potom havaruje)
void main(List<String> args) {
try{
new Future.delayed(new Duration(seconds: 1), (){throw new Exception('uh oh');});
}catch(_){}
new Timer.periodic(new Duration(milliseconds: 100), (_) => print('here I stand'));
}
Nie že by t-c-f bolo pokazené, je to len tá istá logika, ako pri našom funkčnom-nefunkčnom teste: kód dobehol, žiadna výnimka nenastala (ak by áno, t-c-f by ju korektne spracovalo), Futures, a vôbec, budúcnosť, nie sú naša starosť, deň má dosť svojho trápenia!
Inými slovami, vďaka Futures môže hociaký 3rd party kód spôsobiť vyhodenie výnimky, ktorá položí celú našu Dart VM (snáď mi po tomto odpustíte ten patetický nadpis). Dart-isti si tento problém uvedomili a prišli s riešením s názvom runZoned
. Pekná ukážka použitia je napríklad tu. Len čo sa vychytajú bugy podobné tomuto, bude pomocou runZoned
možné dosiahnuť podobný luxus, ako pri písaní synchrónneho kódu dávajú try-catch-finally bloky.
No, není to moc pěkné. Budu-li
Future
chápat jako monádu, tak mohu využít syntaktickou podporu pro monády v některých jazycích a získat tak hezčí kód.C# 5 má speciální podporu pro asynchronní kód – lze psát téměř normální kód.
Otázkou je, proč vlastně nepsat úplně normální kód a nenechat kompilátor / interpretr, aby ho vykonal asynchronně.
Ano, urcite by sa syntax dala sprehladnit, Dart-isti to nastastie aj maju v plane; na druhu stranu, podla toho ake issues maju otvorene, je jasne, ze maju s Dart-om spustu roboty aj bez toho, aby teraz specifikovali a pridavali nove features :)
Na sucasne futures sa celkom da zvyknut, nie je to kod, ktory by som mal chut si vytlacit a zaramovat do obyvacky, urcite sa s tym ale da zit a je to prijemnjesie ako js-callbacky.
Není to moc pěkné,ale v řadě jiných jazyků (ehm, JS, ehm) je to ještě horší :-)
V jakési experimentální větvi dart2js existovala (a možná ještě existuje, netuším) podpora pro C#-like
await
nadFuture
. A kromě http://dartbug.com/104 existuje ještě http://dartbug.com/7002. A i když se zatím neví, jak to nakonec dopadne, nějaká podpora přímo pro asynchronní kód v Dartu bude. Jednu možnost kdysi prezentoval Gilad, viz slajd č. 32 z https://www.dartlang.org/slides/2012/10/html5devconf/dart-today-and-beyond.pdf.Bude seriál pokračovat? Třeba něčím o serveru v Dartu…