Přejít k navigační liště

Zdroják » JavaScript » Dart – Neznesiteľná ľahkosť asynchrónneho bytia

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

Články JavaScript

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!

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

Komentáře

Odebírat
Upozornit na
guest
4 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
Zobrazit všechny komentáře
Radek Miček

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

Ladislav Thon

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 nad Future. 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.

Pavel Dvořák

Bude seriál pokračovat? Třeba něčím o serveru v Dartu…

Přístupnost není jen o splnění norem: nový pohled na inkluzivní design

Přístupnost a inkluze možná nepatří mezi nejžhavější témata digitálního světa – dokud o nich nezačne mluvit Vitaly Friedman. Na WebExpo 2024 předvedl, že inkluzivní design není jen o splněných checkboxech, ale hlavně o lidech. S energií sobě vlastní obrátil zažité přístupy naruby a ukázal, že skutečně přístupný web je nejen možný, ale i nezbytný.

Efektivnější vývoj UI nebo API: Co si odnést z WebExpo 2025?

Různé
Komentáře: 0
Jak snadno implementovat moderní uživatelské rozhraní? Které funkce brzdí rychlost vašeho webu? A kdy raději sami přibrzdit, abychom využitím AI nepřekročili etické principy? Debatu aktuálních dev témat rozdmýchá sedmnáctý ročník technologické konference WebExpo, která proběhne v Praze od 28. do 30. května. Který talk či workshop si rozhodně nenechat ujít? Toto je náš redakční výběr z vývojářských hroznů.

Zapřáhněte AI jako nikdy předtím. Květnová konference WebExpo přivítá hvězdy technologického světa

Od 28. do 30. května 2025 promění pražský Palác Lucerna na tři dny technologická konference WebExpo. Na programu je více než 80 přednášek a workshopů od expertů z celého světa. WebExpo tradičně propojuje vývojáře, designéry, marketéry i byznysové lídry a nabízí praktické dovednosti, strategické myšlení a přináší nejnovější trendy nejen v oblasti AI.