Ondra Bašista
V předchozích článcích seriálu jsem psal o dvou obecně nejznámějších monádách - Either a Option. Nejednoho bystrozora při čtení jistě napadlo - a je to logicky častý use case - zda existují i "monadic" varianty pro asynchronní programování? Odpověď je ano, byť nestačí do monády poslat Promise, dvakrát otčenáš a čekat na data.
Jakákoliv struktura, o které si budeme povídat, stále podléhá standardnímu chování TS/JS u asynchronního kódu - jakmile je asychronní funkce inicializována, je její běh vyjmut z běžného toku programu a lze si jej představit jako v imaginární větvi, oddělené od synchronního světa zbytku kódu. Tento fakt FP-TS žádným hackem neobchází, ale dává programátorům do rukou struktury, které umí podobné funkce/objekty řetězit a mapovat.
Pro ilustraci:
const asyncResult = async() => { return await 'toto je '; }
Pokud provoláme funkci asyncResult, rozhodně nedostaneme výsledek, ale opět Promise.
const anotherAsyncResult = asyncResult().then(preRes => preRes + 'výsledek').then(res => { console.log(res) });
Pokud chceme získat result, musíme se zkrátka také namočit, případně resolve provést ze scope jiné asynchronní entity.
Zda využiji promise jako takovou nebo async/await (není to JEN syntax sugar, ale kombinace promise a generátorů), na problému asynchronní větve nic nemění.
Nicméně, máme za sebou nutný úvod k tomu, abychom za async kódem v FP-TS nehledali nějakou novou magii, v zásadě se jedná o struktury, v tomto případě monády, schopné pracovat s klasickou promise v TypeScriptu. Tak vzhůru na ně.
První monádou budiž struktura jménem Task, která reprezentuje asynchronní komputaci, u které se neočekává selhání. Její typová anotace je jednoduchá, v zasadě se jedná o lazy Promise. Typ tedy vypadá takto:
interface Task <A> { (): Promise <A> }
Task interface je tedy Promise schovaná za funkcí, což už na první pohled dává tušit, že pro získání value není potřeba speciální aparatury ala Either/Maybe. Stačí zkrátka zavolat funkci a použít například await.
Zároveň je ale struktura(implementace) Task monádou - je tedy opět poskládán z klasického API, které extenduje apply(applicative), který zase extenduje funktor atd. Má tedy k dispozici standardní "monadic API" ala map, chain.. Viz první článek seriálu, případně úplný úvod.
Samotným Taskem se prakticky zabývat nebudu a přejdu rovnou ke komplexnější struktuře - TaskEither.
TaskEither je narozdíl od Task komputace, u které lze očekávat záchyt chyby, a jak název napovídá, její interface vypadá a dokonce se přesně tak i chová - jako Either uvnitř Task, neboli Either uvnitř Promise, schovaný za funkcí.
interface TaskEither<E,A> extends Task<Either<E,A>>{}
Pokud tedy zavolám například await taskEither(), získám typ Either<E, A>. TaskEither je monádou, u jeho API tedy platí (obecně) to samé jako u jiných monád.
Pojďme si ukázat, jak s TaskEither pracovat na jednoduchých příkladech.
Nejprve si připravíme důvěrně známý kód:
interface Data { data: { id: string; content: { body?: string; desc: string; }; }; } interface SomeError { msg: string; } const someAsyncFn = async (): Promise<Data[]> => { return [ { data: { id: '1', content: { body: 'some body content', desc: 'some desc', }, }, }, { data: { id: '2', content: { body: undefined, desc: 'some desc', }, }, }, ]; };
SomeAsyncFn tedy vrací Promise a v ní pole typu Data. A nyní pro obalení takovéto funkce - stejně jako v případe Either - využijeme build in funkci TaskEither, tryCatch, která asynchronní funkci obalí do TaskEither monády:
const getSomeAsyncData = TE.tryCatch<SomeError, Data[]>( () => someAsyncFn(), (e) => ({ msg: JSON.stringify(e), }), );
Levá kolejnice tedy (v typu) znázorňuje chybový scénář, pravá data.
Nyní naše data můžeme začít v nějaké pipe řetězit. Připravíme si obyčejnou funkci, u které fail neočekáváme (leda by předchozí TaskEither skončil v levé větvi):
const addSomeNewData = (toAdd: Data) => (data: Data[]) => { return [...data, toAdd]; }
A obě funkce vložíme do pipe:
const result = pipe( getSomeAsyncData, TE.map( addSomeNewData({ data: { id: '3', content: { desc: 'some another desc', }, }, }), ), );
Návratová hodnota result pipe má nyní typ TE.TaskEither<SomeError, Data[]>. TE.map příjmul TE.TaskEither<SomeError, Data[]>, rozbalil value (vzal TaskEither a z funkce vracející Promise vytáhl pomocí then (resolve větev Promise) Either<SomeError, Data[]>), checknul zda není Either v levé kolejnici, a pokud ne - na datech zavolal funkci addSomeNewData a výsledek obalil znovu do Either. Proto tedy náš result vrací typ TE.TaskEither<SomeError, Data[]>. Either za () => Promise.
OK. Co ale dělat v případě, že by naší funkci getSomeAsyncData předcházela nějaký neasynchronní varianta monády, například obyčejný Either. I na to má FP-TS udělátko.
Vyrobíme si tedy nejprve funkcí vracející Either, kterou předsuneme před všechny ostatní v pipe:
const beforeAllFunction = (number: 1 | 2): E.Either <SomeError,number> => { if (number === 1) { return E.right(number); } else return E.left({ msg: 'number is two, error!' }) }
A nyní jí použijeme v pipe a jako přechod mezi Either a TaskEither využijeme opět API TaskEither, konkrétne funkci fromEither, která vezme Either, prověří zdá se nachází ve své left nebo right větvi, pokud v left - vyrobí TaskEither a do jeho levé větve obalí původní chybu, pokud v right - posune do pravé větve novou value.
const result = pipe( TE.fromEither(beforeAllFunction(1)), TE.chain(getSomeAsyncData), TE.map( addSomeNewData({ data: { id: '3', content: { desc: 'some another desc', }, }, }), ), );
Dobrá a nyní poslední varianta. Co kdybych z nějakého důvodu k datům uvnitř TaskEither (pravá větev) potřeboval uvnitř pipe přistoupit a nadále s nima pracovat například v klasické Option monádě? Jak jsem v úvodu článku deklaroval, TaskEither je jako návratová hodnota jen Either obalený funkcí a Promise - jednoduše tedy stačí pomocí then přejít do resolve větve Promise a s value pracovat jako se synchronní verzí Either.
Druhou a zřejmě komfortnější variantou je s daty manipulovat přímo uvnitř chain funkce:
const result = pipe( TE.fromEither(beforeAllFunction(1)), TE.chain(getSomeAsyncData), TE.map( addSomeNewData({ data: { id: '3', content: { desc: 'some another desc', }, }, }), ), TE.chain( flow( O.fromNullable, TE.fromOption(() => ({ msg: 'from option error', })), ), ), );
Pro převzetí payloadu uvnitř posledního chain jsem využil flow, což je alternativa k pipe, která ale využívá partial application. Pro jistotu uvede jednoduchý příklad.
const flowExample = flow((string:string) => `${string} appendix`)('string');
Funkce vrátí value "string appendix".
Opět, jako vždy dodávám, že je API pro asynchronní programování FP-TS komplexnější, existují i další async varianty jako TaskOption nebo TaskEitherReader. Zásadní je ale ukázka FP-TS pojetí asynchronních struktur JS/TS. Žádná magie, naše známé API (monáda, applicative, funktor..) složené do podoby schopné obsluhovat Promise.
V dalším díle se konečně podíváme na různé nástroje pro efektivní řetězení, skládání, shlukování a iterace takovýchto struktur. I () => Promise<Maybe<NextTime>>.
Just leave us a message using the contact form or contact our sales department directly. We will arrange a meeting and discuss your business needs. Together we will discuss the options and propose the most suitable solution.