Technologie jsou naším uměním

Blog

Zpět

Funkcionální programování v TypeScriptu

Technologie

Za poslední dva roky jsme u nás ve firmě (backend psaný kombinací Node.js a TypeScriptu) začali pokukovat po jakémsi semi-funkcionálním pojetí psaní kódu.

Z části nás pravděpodobně zasáhla vlna funkcionálního hype, kterou si troufám tvrdit, do toho opravdového mainstreamu začala tlačit stále mohutnější React komunita. A z části to zkrátka dávalo v ekosystému Node.js, TypeScriptu, GraphQL a microservices smysl.

Podotknu, že valná většina týmu pochází ze světa těch klasických enterprise technologií ala Java/Spring, typicky využívaných spíše v kontextu OOP, DI apod.

Výraz funkcionálnější styl psaní kódu píší záměrně, protože se možná nejedná o (pro namyšleného akademika!) striktně akurátní FP ala Haskell (?). Řekněme ale, že se na pomyslné křivce funkcionálního TypeScriptu dokážeme pohybovat od helper knihoven (Ramda, Lodash..) až po komplexní systém typu fp-ts.

Co vlastně ale to funkcionální znamená? A jak si vlastně takový styl psaní/myšlení vydefinovat?

Nejprve ale malý, zjednodušený slovníček pojmů:

  • algebraická struktura - kanonický objekt s různou komplexitou a předepsaným chováním (laws) - struktury jako monoid, funktor, applicative - představme si jako předpis pro objekt s očekávaným chováním, svého druhu design pattern ve FP
  • kategorie - objekt a morfismus - v TypeScriptu lze vyjádřit morfismus jako f: A => B, kdy f vyjadřuje morfismus z typu A do typu B - v zásadě tedy pár objekt (typ): funkce
  • HKT - higher kinded types - typový parametr, který jako argument přijímá jiný typ ve smyslu - generický (předem neznámý) typ dokáže přijmout jiný typ jako parametr - HKT nejsou součástí TS by default - někdy potřebujeme provádět operace nad generickou strukturou a onu neznámou strukturu osadit zatím neznámým typem
  • currying - <T>(a,b,c) => T aka <T> (a) => (b) => c => T
  • partial application - <T>(a,b,c) => T aka <T> (a, b) => c => T
  • funktor - pro začátek si představme algebraickou strukturu, která je schopná převzít strukturu jinou, rozbalit její obsah, aplikovat na něj funkci (map) a výsledek znovu zabalit do nějaké obálky - dokáže provést mapování mezi kategoriemi
  • monáda - komplexnější algebraická struktura než funktor - oproti funktoru dokáže například aplikovat na data v obálce funkci ukrytou v další obálce

Pokusím se vyhnout akademickým popisům z Wikipedie. To, že je monáda monoid v kategorii endofunktorů, ví přece každý lojza od PHP po Angular :-) . Vynasnažím se tedy podělit o pár poznámek z praxe (nikoliv definici funkcionálního programování), a vyhnout se frázím typu "deklarativní vs..", protože deklarativní je dnes zkrátka kde co.

Více přemýšlíme o strukturách, které s daty manipulují, nežli o datech jako takových

Pokusím se načrtnout jednoduchý příklad, je ale třeba si uvědomit, že se jedná o kousek ilustrativní, protože jádro nad přemýšlení o FP strukturách může být o level abstraktnější - ve strukturách algebraických.

interface Washable {
  wash: () => ...,
  clean: () => ...,
}

interface Openable {
  open: () => ...,
  close: () => ...,
}

const vehicle: Washable & Openable = {
  wash: () => ...,
  clean: () => ...,
  open: () => ...,
  close: () => ...,
}

const table: Washable= {
  wash: () => ...,
  clean: () => ...,
}

const tableWithDoors: Washable & Openable = {
  wash: () => ...,
  clean: () => ...,
  open: () => ...,
  close: () => ...,
}

const openAndWash = (entity: Washable & Openable) = {
  return entity.open().then(res => res.wash());
}

openAndWash(vehicle); 
openAndWash(tableWithDoors); 

// bad
openAndWash(table);

Jak je z obrázku patrné, funkci openAndWash nezajíma zda otevírá dveře u vozidla nebo od stolku, podstatná je pouze spolehlivost struktury - každá Washable struktura je schopná aplikovat clean. Stejně jako je funktor schopný vzít libovolná data z wrapperu, aplikovat na ně funkci a znovu je do obálky zabalit. A to je kánon.

Ona dělící čára se může zdát nepatrná, ale přemýšlet "data agnostic" dává řádově větší smysl ve chvíli, kdy se začneme zabývat strukturou abstraktnější, která vůbec neimplikuje povahu dat, ale nabízí jen různé způsoby manipulace s jinou strukturou.

Pokud si totiž za typem Washable představíme právě strukturu typu funktor - která je stejně jako Washable schopná manipulovat s daty skrze očekávatelný pattern, dostaneme se o něco blíž jádru pudla.

O algebraických strukturách (funktory, monády) a komplexnějších typech si povíme v nějakém příštím blogu. Poselství je však závěrem následující: funkcionálně naladěný programátor více než o datech přemýšlí o vztahu mezi strukturami, které s daty manipulují.

Řetězíme, řetězíme!

const cleanMyCar = <A>(car:A) => pipe(
    car,
    openDoor,
    cleanDoor,
    closeDoor,
    cleanWindow,
    (car) => `${car} is now clean!`
)const cleanMyCar = <A>(car:A) => flow(
    openDoor,
    cleanDoor,
    closeDoor,
    cleanWindow,
    (car) => (`${car} is now clean!`)
)(car)

Obrázek je poměrně popisný - jestli je pro FP něco (nejen) vizuálně typické, je to řetězení funkcí do postupně navazujícího celku či skládání jednodušších funkcí do komplexnějších.

Využití různých pipe se může zdát zaměnitelné s klasickou deklaraci proměnných, všiml jsem si ale že vede (navádí, přímo neimplikuje) k následujícímu:

  • Vhodnější pojmenovávaní funkcí - řetězené funkce přímo volají po přesném a popisném pojmenování
  • Kratší funkce a jednodušší funkce - řetězené funkce v kombinaci s pojmenováním volají o jednom jediném účelu (single responsibility principle / single purpose function) - žádné víceúčelové master funkce
  • Zápis je přehledný a jasně deklaruje flow událostí
  • Funkce jsou v JS/TS tzv. first class objekty - je možné je využít jako návratového hodnoty i parametry jiných funkcí - což je v kombinaci s curryingem či partial application, velmi komplexní nástroj pro flexibilní řetězení

Composition over inheritance

Pradávná mantra neplatí jistě jen pro FP, ale opakování je matkou moudrosti. Skládáme, protínáme, vyjímáme, nedědíme. Komplexnější funkce vznikají kompozicí samostatně testovatelných jednodušších celků. Netvoříme božské monolity, ale variabilní kousky stavebnice.

const postService  = <A,B,C>() => ({
  ...withSendMessage,
  ...withCreateMessage,
  ...withReadMessage,
  ...withListItems,
  sayHello: () => 'hello! :-)'
})

A kde jsou co sakra ty monády?!

Vypíchl jsem tři z mého pohledu zásadní body pro přemýšlení o funkcionálnějším stylu programování, aniž bych záměrně zabrousil přímo do komplexnějšího světa higher kinded typů a implementací struktur jako jsou monády.

Hlubiny TypeScriptu jsou ale mnohem barevnější, než by se mohlo na první pohled zdát a v dalších článcích si povíme něco o nástrojích, které takové programování umožňují na úrovni mnohdy komplexnější než u jazyků proklamovaných jako funkcionálních (např. Elm bez podpory HKT).

Funkcionální JavaScript totiž nemusí být jen lambda, map, reduce a filter. Takže někdy příště o FP-TS, higher kinded typech, funktorech, monádách a typových třídách, side efektech, error handlingu a asynchronním programování, protože právě tam někde se nachází ono příslovečné "Heuréka!".

P.S. Autor nepíše na ledničku vzkazy v Haskellu a vyhrazuje si právo mírného zkreslení matematických pojmů apod.

Sídlo firmy - Praha

Tel: +420 602 609 112E-mail: info@apitree.cz

ApiTree s.r.o.
Francouzská 75/4Praha 2 Vinohrady120 00

Údaje o společnosti

Společnost ApiTree s.r.o je zapsána v obchodním rejstříku u Městského soudu v Praze, pod spis. zn. C 279944

IČ: 06308643
DIČ: CZ06308643

Bankovní spojení

Číslo účtu: 4885827379/0800
Česká spořitelna

IBAN: CZ21 0800 0000 0048 8582 7379
SWIFT: GIBACZPX

Copyright 2020 ApiTree s.r.o. Všechna práva vyhrazena. Web vytvořilo a designovalo ApiTree s.r.o.

Tento web používá k analýze návštěvnosti soubory cookies. Používáním tohoto webu souhlasíte s ukládáním a používáním souborů cookies. Více o ochraně osobních údajů.