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ů)

  1. Dart – Čo? Prečo? 2.8.2013
  2. Dart – Úvod do jazyka 23.8.2013
  3. Dart – Ponorme sa hlbšie 6.9.2013
  4. Dart – v DOMe 19.9.2013
  5. Dart – Futures 4.10.2013
  6. Dart ­– Streams 17.10.2013
  7. Dart – Používame JavaScript 1.11.2013
  8. Dart Typesystem 19.11.2013
  9. Dart – Neznesiteľná ľahkosť asynchrónneho bytia 2.12.2013

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 ajaxu 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.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 4

Přehled komentářů

Radek Miček
Tomáš Kulich Re:
Ladislav Thon Re:
Pavel Dvořák Pokračování?
Zdroj: https://www.zdrojak.cz/?p=10477