Proč Clojure?

Proč Clojure?

Před nějakými 12, 10 lety byl v Česku obrovský hajp funkcionálního programování. Na všech vývojářských konferencích se mluvilo o funkcionálním programování. Taky musím zmínit Lambda Meetupy, které mimojiné poháněl Dan Škarda (díky).

A přesně na těch meetupech jsem se přesvědčil, že má Clojure smysl. Následně jsem v Barceloně (tehdy jsem pendloval mezi Prahou a Barcelonou 2 týdny tam a 2 tam) taky založil Clojure meetup group (https://www.meetup.com/ClojureBCN/), která se dodneška setkává.

Clojure mě celou tu dobu nepustilo a ikdyž nemám čas programovat tolik, kolik bych si přál, vidím v tom jazyce pořád obrovský smysl.

Ve zkratce a bez mnoha ukázek kódu, bych chtěl dnes vysvětlit, proč by lidi měli stavět své aplikace v Clojure.

Clojure je funkcionální programovací jazyk. Funkcionální jazyky jsou zpravidla velmi expresivní. To, co je v jiných jazycích na několik řádek kódu, to je ve funkcionálních jazycích často jen jedna řádka kódu.

To je způsobeno několika důležitými aspekty:

  1. High Order Functions
  2. Funkční kompozice
  3. ne-encapsulation

High-order-functions

High-order funkce jsou funkce, které jako svůj parametr očekávají další funkce, nebo funkce, které funkce vrací. Tím vytváříte funkce, které jsou výrazně konfigurovatelné.

Tři nejdůležitější funkce jsou map, filter, fold.

Map vezme kolekci a předanou funkcí změní všechny prvky dané kolekce. Tedy list (1 2 3) a map (+ 1) vrátí list (2 3 4).

Filter vezme kolekci a vyhodí všechny prvky, které neodpovídají dané funkci). Tedy list (1 2 3) a filter (> 1) vrátí list (2 3).

Fold vezme kolekci, redukční funkci a proměnnou, kam si schovává mezivýsledek a vrátí poslední mezivýsledek. Takže budeme-li implementovat třeba součet, budeme mít list (1 2 3 4), fold (+) a ono to vrátí součet 10.

Obrovská výhoda high order funkcí je to, že jsou konfigurovatelné. Takže když budu mít list ({”item”: 1} {”item”: 2} {”item”: 3} {”item”: 4}), tak můžu napsat snadno výraz, která třeba inkrementuje +1 takto:

(defn increment-items [maps]
  (map #(update % :item inc) maps))

(def maps [{:item 1} {:item 2} {:item 3} {:item 4}])
(increment-items maps)

Clojure má strašnou spoustu high order funkcí pro procházení a modifikaci kolekcí, stromů, rozdělování kolekcí, group by, řazení, zipování, ale i generování nových kolekcí a datových struktur atd.

Funkční kompozice

Funkcionální programování a Unix má společný koncept. Mnoho malých příkazů, které svou práci dělají dobře a cestu, jak je pospojovat.

V UNIXech se používají pipes a vše je string. V okamžiku volání se výstupy z jednoho příkazu posílají jako vstupy do dalšího. To samotné je velmi silný koncept.

Ve funkcionálních jazycích je tento koncept mnohem silnější:

  1. Výstupy ani vstupy nemusí být string, ale naprosto jakákoliv datová struktura.
  2. Ta kompozice nevzniká ad hoc jen v okamžiku, kdy ji volám. Můžu si vytvářet částečné kompozice a ty pak rozšiřovat a znovupoužívat. V Linuxu nemůžu jen ta schovat kus výrazu sort | head -n 10, naopak ten kousek musím kopírovat dokola, nebo si napsat nový příkaz, který toto dělá.

Nejdůležitější funkce v Clojure pro funkční kompozici, je funkce comp , ta slouží pro spojení funkcí tak, že vrátí funkci. Další kompozice jsou ->, ->>, které vykonávají běh okamžitě a které jsou podobné pipe v Unixu. Funkce comp se často kombinuje s funkcí partial, která z funkce s neúplným počtem parametrů vytvoří funkci, která čeká na ty chybějící parametry (tzv. partial application).

Ukázka:

; Varianta 1 pouzivajici ->> a okamzite posilani dat
(defn sort-and-take-top-10 [maps]
  (let [sort-desc (comp - :importance)]
    (->> maps
         (sort-by sort-desc)
         (take 10))))

; Varianta 2, kdy si pomoci comp vyrobime funkci     
(defn sort-and-take-top-10 [maps]
  (let [sort-desc (comp - :importance)]
    ((comp (partial take 10) (partial sort-by sort-desc)) maps)))
 
(def maps [{:name "Task 1" :importance 5}
           {:name "Task 2" :importance 3}
           {:name "Task 3" :importance 10}
           {:name "Task 4" :importance 7}
           {:name "Task 5" :importance 2}
           {:name "Task 6" :importance 8}
           {:name "Task 7" :importance 6}
           {:name "Task 8" :importance 1}
           {:name "Task 9" :importance 9}
           {:name "Task 10" :importance 4}
           {:name "Task 11" :importance 11}
           {:name "Task 12" :importance 0}])

(sort-and-take-top-10 maps)

Varianta 1 je bezprostřední posílání dat z jedné funkce do druhé. Ta varianta 2 je zajímavá v tom výrazu (comp (partial take 10) (partial sort-by sort-desc)) maps) - všimněte si, že v ukázce je tento výraz obalem závorkami dvakrát. To proto, že první výraz vrací funkci a druhý ji rovnou volá.

Ale já nemusím tu funkci volat. Mohl bych ji vrátit a měl bych definovanou funkci: “seřaď podle importance desc a vrať 10” a mohl bych ji používat v dalších comp, -> nebo ->> a komponovat takto kusy funkcionality.

Ne-encapsulation

Tohle je asi bod, na kterém naštvu OOP puristy. Clojure a Lispy obecně mají zvyk používat několik základních datových struktur a nad nimi stavět celou aplikaci. Máte individuální hodnotu, list (spojový seznam, hledání stojí N, přidání 1), vektor (hledání stojí 1, přidání N), hash map a set (kolekci, kde může být položka pouze jednou). Vše tedy okořeněno tím, že Clojure extrémně využívá lazy sequence, ale to je implementační detail, který je do určité míry skryt.

Těchto několik datových struktur se používá úplně na všechno.

Data o zákaznících? Adresa? Položky z databáze? Hash mapy v listu. Pokud je čtu z databáze, pravděpodobně v lazy sekvenci. Ale funkce na čtení a kód budou stejné.

Ano, Clojure má způsoby, jak definovat typy, Data Transfer Objects, ale používá je na interoperabilitu s JVM. Většinou je nepotřebujete.

Souběžně s tím, že tedy nevytváříme typy, nezabalujeme typy a funkce, které s danými typy pracují, do společných funkcí. V nějakém OOP jazyce bych mohl napsat kód typu ordersRepository.readOrders().sortBySize().last a budou to metody. readOrders metoda bude v OrdersRepository class. sortBySize metoda bude v OrdersResultSet classe. last pravděpodobně dostanu zadarmo, když OrdersResultSet zdědí z nějakého systémového řešení pro kolekce.

V Clojure napíšu podobný kód (-> ordersrepository/read-oders (sort-by :size) last . Ten rozdíl je v tom, že poud bych si definoval třeba tu funkci sort-by, tak tak funkce je univerzální. Dost možná lze použít v 1000 různých místech. Zejména, pokud funce high order funkce.

V Clojure máte málo datových struktur, ale hodně funkcí, které s nimi pracují.

A my Clojuristé věříme, že je lepší mít 5 datových struktur a 50 silných funkcí nad každou, než mít 50 datových struktur a 10 funkcí nad každou.

Tím, že přidáváte další funkce, které pracují se základními datovými strukturami, rozšiřujete programovací jazyk. Pokud se vám v projektu například často objevuje hash-map v hash-mapě v listu, vznikne vám časem skupina funkcí, které jsou skvělé na práci s hash-mapou v hash-mapě v listu. Funkce pro přidávání, hledání, editaci, groupování. A ty funkce pak použijete na mnoha různých místech.

Krátké shrnutí

Takže tedy jeden aspekt toho, proč je Clojure silný jazyk, je samotný fakt, že máte k dispozici spoustu silných high-order funkcí, můžete je do sebe skládat, k tomu několik málo datových struktur, pro které existuje mnoho funkcí (řada z nich jsou HOF) a že je zvykem si psát vlastní HOFs, vlastní funkce rozšiřující práci s daty a vše toto komponovat.

Hezky napsaný Clojure kód vypadá, jako kdyby vysloveně data tekla, transformovala se a ukládala nebo vypisovala nebo vracela a vše tak, jako kdyby ten jazyk byl od začátku pro něco takového určen.

To je spojené s větším důrazem na dobré navržení toho, v jakém tvaru ta data mám. Myslím si, že tam musí Clojure programátor dávat větší pozor, než programátoři v jiných programovacích jazycích (kde obecně lidem nevadí napsat několik vnořených foreachů, posbírat si mezivýsledky do růzých mezikolekcí). Ale o tom jindy.

Další aspekty

Existuje celá řada dalších důležitých aspektů, které zmíním v rychlosti, ale které jsou taky naprosto zásadní pro uživatelský zážitek při práci s Clojure.

  1. JVM ekosystém - nesetkávám se s tím, že by na něco nebyla knihovna. Většinou existuje knihovna přímo pro Clojure. Pokud ne, je možné importovat javovskou knihovnu, obalit ji trošku, aby API bylo funkcionální, ne objektové, byť Clojure objektově psát jde. Pro Javu existují knihovny snad na všechno, co vás napadne. Tak tyto knihovny jsou pak k dispozici i pro Clojure.
  2. ClojureScript - můžete mít Cljs i v browseru. To je velmi pohodlné hlavně ve spojení s tím, že v Clojure lze HTML reprezentovat jako datovou strukturu. Takže můžete snadno psát funkce, které dostávají HTML, generují HTML, a vůbec u toho nelepíte žádné stringy. Data dovnitř, data ven.
  3. Kontrola datového schématu - Clojure je dynamický jazyk. Ale upřímně jsem roky neviděl Clojure projekt, který by nepoužíval nějaký nástroj na kontrolu schématu dat. To jsou v podstatě validační funkce, které dostanou datovou strukturu, předpis toho, jak mají být data formátována (příklad: list hash map s klíči X - string, povinný, Y - list čísel nepovinný, Z - list Userů - definováno jiným schématem, nepovinný klíč, ale pokud je klíč, je povinná hodnota). Toto je extrémně užitečné, zejména když zajišťujete konzistenci toho, co leze z databáze, nebo co očekává šablona.
  4. REPL-oriented development - v Lispových jazycích je běžný způsob vývoje tak, že vám vlastně aplikace běží, vy do ní posíláte výrazy a rovnou je vyhodnocujete. U toho máte k dispozici celou tu běžící aplikaci a skvělou podporu IDE. Troufnu si říct, že ač by teoreticky REPL-oriented development mohl fungovat kdekoliv, funkcionální jazyky jsou pro toto mnohem vhodnější, protože je zvykem izolovat side effecty a vnitřní stav, výměna funkce není obvykle riskantní, zatímco výměna metody, nebo objektu v OOP může být problematičtější kvůli více vnitřních závislostech, dědičnosti atd.
  5. Clojure komunita a financování - Clojure má skvělou komunitu, bez toho, abych přeháněl, jedni z nejlepších vývojářů, jaké jsem kdy potkal, používají Clojure. Není divu, že i Uncle Bob (autor knihy Čistý kód a legenda programátorského světa) Clojure chválí, kde může a považuje ho za nejlepší jazyk. Zároveň Clojure financuje nubank, megaúspěšná brazilská firma. Clojure má jistotu, že to autora nepřestane bavit nebo mu nedojdou peníze (ve skutečnosti autor Clojure Rich Hickey má podle všeho peněz dostatek až do smrti a chce se zaměřovat vysloveně na vývoj open source - Clojure a ekosystému okolo).

Nevýhody Clojure

Clojure má i dvě nevýhody.

Je obecně méně lidí, než v jiných jazycích. Ale ve Flexianě jsme za ty roky už vypiplali přes 200 Clojure vývojářů a pomohli mnoha firmám na Clojure přejít.

A druhá nevýhoda, kterou Clojure sdílí s JavaScriptem, že je to svět devstacků, ne full frameworků. Já vím, že pro některé typy aplikací je klasický MVC framework ta nejlepší volba. To taky řešíme a vyvíjíme MVC framework pro Clojure: Xiana. Věci, jako routování, auth, databáze, migrace, MVC, napojení na frontend, sessions, atd. jsou všechny řešeny. Navíc používáme mainstreamové knihovny, takže se na náš framework snadno migruje a v případě potřeby se dá migrovat i z něj.

Jak začít s Clojure?

Miluju knihu The Joy of Clojure. Ta je sice už dost stará, ale skvěle napsaná a je vynikající úvod do toho, jak v Clojure programovat.

Další způsob, jak si potrénovat Clojure, jsou Clojure Koans.

Vyplatí se naučit se Emacs, protože ten používá většina Clojure komunity. Pokud nechcete dělat v Emacs, tak například pro VSCode existuje skvělé rozšíření Calva.

Pokud vyvíjíte webové aplikace, dává smysl zkusit Xianu, nebo alespoň knihovny, které v Xianě jsou, protože jsou dobrý default a naprostý mainstream v Clojure světě. List najdete zde.

Pokud budete v Clojure nějakou dobu pracovat a budete chtít v něm získat práci, nebo pokud chcete překlopit nějakou svoji aplikaci do Clojure a potřebujete s tím poradit, tak mi klidně napište na jiri@flexiana.com.

A samozřejmě dává smysl sledovat Prague Lambda Meetupy (které nejsou jen o Clojure, ale o funkcionálním programování obecně, byť Clojure určitě dostává hodně prostoru). Nebo vyrazit na nějakou z Clojure konferencí. Například toto září v Belgii bude Heart of Clojure.