Takže, rozhodl jsem se napsat něco jako moje vyjádření k FP a Haskellu, a to pak budu různě v diskuzích tapetovat, jinak to nejde, protože vždy když přijdu bez něčeho takového, tak nedokážu udržet konzistenci argumentů, a protože mám nezdravou vlastnost snažit se akceptovat názory ostatních (ostatní to ale tak necítí, a spokojeně kecají i o tom, o čem mají omezený přehled), dopadne to nakonec tak že říkám píčoviny jako že třeba IO monáda je čistá v jednom vlákně. Hovno. Takže tak, napsal sem fakt román, a pište svoje názory či otázky až poté, co se nad problémem zamyslíte. Omlouvám se za útoky a vulgarity, a dokonce se omlouvám i za to co teď řeknu, ale nemůžu si pomoct, každý kdo si myslí že je možné něco jako forkWorld v Cleanu je debil který nepochopil ani první větu na wiki o UT ale už se cítí plně kompetentní o tom diskutovat a zpochybňovat názory někoho kdo nad touto problematikou přemýšlel dlouhé dni. Ta první věta je "In computing, a unique type guarantees that an object is used in a single-threaded way,
with at most a single reference to it." Ano, forkWorld by udělal dvě reference a tím by bylo porušeno pravidlo UT (a objekt by byl použit v multi-threaded way). Achjo
Fakt, je mi to líto, ale už sem alergický na hlupáky, kteří ačkoli o tom nic neví, snaží se z člověka udělat debila a jeho dobrý nápad zakopat tím, že ho zahrnou kopou hloupých argumentů, a nebožák se v tom ještě sám zaplete. Vysral bych se na osvětu, měl bych víc času a míň nervů, ale FP si to zaslouží.
No a teď k tomu mého dlouhatáááánskému cancu ...
Mé stanovisko k FP a Haskellu
Tento, řekněme blok textu se bude dělit na několik částí, přičemž použité principy se budu snažit vysvětlovat. Není horší diskuze, než s člověkem který ačkoli zná určitou metodu 2 hodiny, cítí se plně oprávněn dělat závěry.
1) Co to je FP
2) Co může přinést FP
3) Řešení IO v Haskellu
4) Stručné shrnutí dalších možných metod řešení IO ve FP jazycích
5) Základní myšlenka uniqueness typing
6) Základní myšlenka FRP (reaktivní FP)
7) Základní myšlenka lazy listů (landin stream IO)
1) FP je paradigma, které je inspirované matematickou teorií funkcí a celkově podobou a chováním matematických výrazů. Jak se chovají matematické funkce ? Nemají žádný vnitřní stav, jediný vstup mají skrz své argumenty a jediný jejich výstup je hodnota kořenového výrazu funkce. Matematická funkce se stejnými argumenty vrátí vždy stejný výsledek. Je jedno kolikrát ji vyhodnotíme a nebo kdy ji vyhodnotíme. Matematická funkce je absolutně deterministická. Jak dosáhnout těchto parametrů v programovacím jazice ? Napřed musíme všechna data zmrazit, žádný objekt nesmí být po vytvoření měnitelný, v matematice také nezměníme již jednou přiřazenou hodnotu argumentu. Tím dosáhneme, že nemůžeme pomocí datové mutace výsledek funkce poslat skrz vstupní argumenty. Jako další krok musíme zajistit, aby funkce za žádnou cenu neměnila globální stav světa či běhového prostředí. Žádná matematická funkce něco takového neumí. Ale kdyby uměla, bylo by to fakt ale fakt drsný, jen si to představte, napíšete matematickou funkce, a postupně jak ji řešíte tak se třeba postaví dům (a nebo třeba smahnete někoho bleskem). Cool, vysvětlil sem magii. Ale tak to v našem vesmíru nefunguje. Teď zase vážně. Funkce, které splňují podmínky matematické funkce se nazývají čisté funkce, a jazyk je FP (dnes pure FP ...) pouze za předpokladu že všechny jeho funkce jsou čisté funkce.
2) FP díky tomu že má mnoho, řekněme omezení nabízí něco jako výměnu. Jsou to jistoty. A když máme jistoty, můžeme plno věcí optimalizovat a silně zrychlit. Takže. Začnem pěkně odzačátku.
- FP jazyky díky tomu že mají velmi vysokou úroveň abstrakce silně zkracují délku kódu. Zhruba 5 až 10. Tím usnadňují čitelnost a udržovatelnost.
- FP jazyky díky své vyšší abstrakci umožňují velmi snadno řešit problémy, které by s nižší úrovní abstrakce byli trošku problém.
- Díky determinismu usnadňují přemýšlení nad funkcí programu, což vede k menšímu počtu chyb
- Možnost silné optimalizace GC. GC ve FP jazycích je právě díky poskytovaným jistotám mnohem rychlejší, než GC v jazicích s měnitelným stavem. Na druhou stranu, FP jazyk k funkci potřebuje GC, protože přímé operace s pamětí nejsou čisté. A neměnná data potřebují k efektivní funkci GC taktéž. U nižších paradigmat GC ale není podmínkou, takže je to výhoda sporná.
- Možnost nahradit výraz jeho hodnotou bez porušení funkce programu. Řečeno stručně, můžeme cachovat výsledky funkcí, a tím na úkor paměti zrychlit vykonávání programu. Geniální a jednoduché.
- Možnost využití nestriktních vyhodnocovacích modelů funkcí, které není možné implicitně a bezpečně aplikovat v jazycích s měnitelným stavem a přesně danou sekvencí vykonávaných operací.
| call by need - možnost využívání nekonečných datových struktur, šetření procesorového času (obvykle nikoli) a paměti
| call by future - možnost implicitního paralelního vyhodnocování argumentů funkce bez nutnosti zamykání paměti (paměť se nikdy nemění).
... a mnoho dalších
- Právě call by future je nejsilnější schopnost FP, protože je možné psát implicitně plně paralelní programy bez žádných složitých a těžkopádných caviků známých z paradigmat s měnitelným stavem (kód není třeba nijak zvláštně modifikovat, všechno prostě funguje "samo"). V dnešní době se ale o nic podobného žádné runtime nepokusilo a navíc nemáme žádnou FP architekturu. Není ale nutná, neříkám že runtime s touto schopností by bylo snadné vytvořit, ale je to možné (napřed by tady ale musel být SKUTEČNĚ pure FP jazyk ...). Proč je ale schopnost implicitního paralelismu v jiných paradigmatech zapovězená? Funkce mají vedlejší účinky, a tyto vedlejší účinky se často musí provést v nějaké sekvenci, rozhodně ne paralelně. A jako další důvod zde máme měnitelnost dat a nutnost zamykání pro zabránění souběhu. Dokud máme málo jader, zamykání nesežere více než je zisk výkonu přidaného jádra. Čím je ale jader více, tím horší je mezi nimi synchronizace a zisk za každé přidané jádro se snižuje, až nakonec od určitého počtu jader začneme výkon místo zvyšování snižovat. Dnešní silně paralelní systémy to řeší rozdělením paměti na bloky, ale je to takové polovičaté řešení které komplikuje programování a snižuje potencionální výkonovou výtěžnost systému. Díky neměnnosti paměti ve FP ale není třeba nic zamykat, a tak můžeme mít v systému kolik jader chceme, výkon se bude jen zvyšovat. Dále díky absenci vedlejších efektů funkcí ani nezávisí na pořadí jejich vykonání. Jak jsem řekl nazačátku, implicitní neomezený a automatický paralelismus je nejsilnější zbraní FP.
---
jak vidno, FP má opravdu velmi mnoho výhod, a nedodržení podmínek FP nás v jazycích pak stojí vysokou cenu. Všechny (nebo aspoň ty nejlepší) tyto výhody.
3) Haskell se rozhodl vyřešit problémy s IO svérázně pomocí monád. Monáda je interface, která umožňuje vyjádřit sekvenci operací v nějakém prostředí. Prostředí může být obyčejný list a nebo celí svět. Sama o sobě monáda není nečistá. Její sílou je právě zajištění sekvence operací v prostředí, což se pro IO perfektně hodí. V Haskellu IO monáda není a ani nikdy neměla být čistá. Autoři Haskellu na čistotu tak trošku zanevřeli, protože podle nich čistě funkcionální program je jako černá krabice, jediné co můžeme říct je to, že se zahřívá. IO Monáda nás nezbaví vedlejších efektů, IO monáda ty vedlejší efekty zavře do klece aby neunikly do zbytku čistého kódu. Nic jiného neumí. A ani nikdy umět neměla. IO monáda zezačátku sloužila pro prosté IO, pak se přidaly měnitelné reference, které musí být zamykané, pokud k nim přistupujeme z více vláken. S měnitelnými referencemi se objevil další problém. Funkce může svůj výsledek poslat skrz své vstupní argumenty (silné porušení FP). A co je nejhorší, Haskell nás k tomu i nutí, prototže pokud chceme provádět IO ve více vláknech, musíme nějak výsledky z těchto vláken dostat. No, a samozřejmě jak jinak, uděláme to pěkně imperativně skrz jejich vstup. Protože Haskell nemá žádný použitelný FP GUI knihovnu, jsme odkázání na porty různých imperativních knihoven postavených na callbackách, no a aby ty callbacky k něčemu byli, musíme opět použít menitelné reference. Dále není pravda, že IO monáda je v jednom vlákně čistá. Není, sice nevznikne data race a měnitelné reference nemusíme zamykat, ale právě ony měnitelné reference boří celé FP, protože již nejde očekávat stejný výsledek při stejném vstupu. A nakonec, jakákoli funkce která pomocí IO monády dělá vedlejší efekty rozhodně nemůže být nahrazena svou návratovou hodnotou. Proč? Protože výstup těchto funkcí může být takřka kdekoli, a nebo klidně skrz vstupní argumenty. A také se v závislosti vnějšího prostředí výstup funkce se stejnými argumenty mění. Uzavřít je to možno tak, že Haskell ani nikdy neměl být čistý, měl být prostě dostatečně dobrý a relativně snadno implementovatelný. Tu jeho vlastnost "dostatečně dobrý" je možno dokumentovat i v případě optimistic evaluation (něco jako lazy evaluation akorát na steroidech). Optimisitc evaluation dávalo oproti lazy evaluation zhruba 30% lepší výsledky. Nikdy se ale nedostalo dál než jen k experimentům. Pro Haskell to prostě byla zbytečná snaha, už tak byl dobrý, co si komplikovat práci. Je obtížné zjistit na internetu vyjádření k tomuto, jednou se mi ale podařilo na Haskell cafe najít vysvětlení že prostě je to moc komplikované (ve zkratce, jsme líný). Haskell je rozhodně moc hezký jazyk, ale autoři lžou. Není to pure FP jazyk, pouze jen FP jazyk (toto označení se bohužel vžilo pro všechny funkcionální jazyky s vyjímkami čistoty, a nejen pro ně, jsou idioti kteří jsou schopni říct že FP je i Python, či jakýkoli jazyk s funkcemi že ...).
4) Ony ale existují cesty na IO se zachováním čistoty. A nepochybně existuje mnoho dalších, neobjevených.
- Uniqueness typing
- Reactivity
- Lazy message streams (aka landin streams)
5) Základní myšlenka UT je ta, že na měnitelný objekt může ukazovat jen jedna jediná reference, ne více. Lze si to představit jako kdyby jsme spojili měnitelnou hodnotu s parametrem času. Takováto měnitelná hodnota může být jen v jednom vlákně, už z logiky věci, kdyby byla ve více vláknech, byla by porušena podmínka UT jedné reference. Z toho důvodu je něco na způsob forkWorld nemožné protože by to nedovolil typový systém (v Haskellu forkIO neporušuje pravidla monád, to jen rýpnutí). Co z toho plyne? Měnitelná hodnota se stane po jednom použití nepoužitelná, a aby jsme ji mohly použít, musí někde vzniknout nová reference na její verzy v čase t+1. Tím je zajištěno, že ačkoli funkce dělá vedlejší efekty, jsou svázané s časem, a protože se neumíme vrátit v čase zpět, nemůže se nečistota nikdy projevit (funkce dělající vedlejší efekty není nikdy zavolána vícekrát se stejnými parametry, nemůže). UT je spíš ochcání čistoty než její řešení. Její hlavní nevýhoda je IO jen v jednom vlákně, a to je nepochybně důvod, proč se s UT v budoucnu nikdy ve vetším jazyce nepotkáme.
6) Reaktivita. V dnešní době se jedná o nejlepší řešení. Čisté funkce usměrňují a transformují signály v uzlech grafu signálů. Vstupy a výstupy grafu signálu můžou běžet v oddělených vláknech a i samotné transformační uzly mohou běžet ve svých vláknech. Tak jako GC přesunulo management paměti na runtime místo na jazyk, tak reaktivita přesune IO z jazyka na runtime. Výsledek? Píšeme čistě funkcionální kód bez poskvrnky, o IO se stará sám systém, mi jen funkcionálně modifikujeme proudy signálů. A samozřejmě, mohou se plně projevit všechny FP výhody. Pro bližší studium navrhuji kouknout se na jazyky na návrh EL obvodů a nebo na jazyk Elm.
7) Poslední metoda funguje trošku podobně jako reaktivita. Vstupní funkce bere jako argument nekonečný list zpráv a vrací nekonečný list odpovědí, o IO se opět stará runtime, nikoly jazyk. IO může běžet ve více vláknech, podle chuti runtimu, a mi pracujeme jen s čistým kódem který může využít všech FP výhod.