Skoda, ze v knizce chybi Haskell, protoze to je "enfant terrible" v FP. To ji v mych ocich dost degraduje.
Byl jsem dlouho odpurcem FP, ale nakonec jsem skoncil (po kratkem koketovani se Scalou a Clojure) u Haskellu. Dlouho jsem premyslel nad tim, cim se FP a OOP lisi, a co to znamena pro architekturu. Nakonec, diskuse s kolegou o OOP, znamenala pro me prulom v teto otazce.
Moje soucasna domnenka - OOP a FP se lisi v pojeti rozhrani mezi castmi programu, a ve zpusobu zapouzdreni dat. V OOP jsou rozhranim volani metod objektu, tedy akce, ktere ma cilovy objekt provest. Data se pritom snazime zapouzdrit do objektu. Naproti tomu v FP jsou rozhranim samotna data (v podstate typy).
Svym zpusobem jde o dualni pristup. V OOP predavame v ramci casti programu data zapouzdrena v objektech, v FP predavame (opacnym smerem) funkce, ktere nad temi daty pracuji. IMHO kazdy design pattern v OOP lze prevest do FP tim, ze misto predavani objektu budeme predavat funkce opacnym smerem. Take se na to lze divat jako na zmenu vztazne soustavy - v OOP jsou funkce na miste a data se pohybuji, zatimco v FP se pohybuji funkce a data stoji na miste.
Proto se tak FP uplatnilo v MapReduce paradigmatu (a jeho naslednicich), ktere je cele o tom, predavat mezi distribuovanymi uzly operace k datum misto samotnych dat. A v podstate i puvodni Unixove paradigma rour je dost funkcionalni, akorat ta datova rozhrani jsou ad hoc, vetsinou textova.
Myslim, ze pristup FP je lepsi, protoze:
- Data se lepe semanticky definuji nez operace. Pokud jsou tedy rozhranim data a ne operace, je snazsi tomu rozhrani porozumet.
- Data jako rozhrani davaji vetsi prostor ke znovupouzitelnosti. Nad objektem, ktery data skryva, jsme omezeni akcemi, ktere dovoluje jeho rozhrani. V FP ale k danym datum muzeme provadet libovolne operace. To je hezky videt prave na Unixovem pristupu, ktery je asi nejlepsim uspechem modularity v pocitacich vubec.
- V OOP se casto stava, ze potrebujete data prece jenom predat, a to pak znamena spoustu operaci, ktere jsou v podstate identitou. V FP tohle odpada, protoze je zrejme, ze nekde predavate identickou funkci.
To neni moc podstatne, v kazdem paradigmatu potrebujete nejakou kombinaci funkci a dat. Spis jde o to, jak se pracuje se "zdroji" dat.
Nejlip tenhle rozdil vynika v Haskellu, ale objevuje se do jiste miry ve vsech FP jazycich. V OOP se zdroje dat skryvaji do malych jednotek, objektu. To co se predava mezi castmi programu jsou prave odkazy na zdroje. V pure FP (jako ma Haskell) to tak neni, tam si zdroje drzi centrum, ktere misto toho dostava instrukce ve forme treba monadickych typu.
Obecne se z toho pak stava rozdil ve zpusobu, jakym se pracuje s menitelnym stavem (coz je v podstate forma zdroje dat). V OOP se ten stav zvlada tim, ze se schova pod nejake API operaci nad nim. Tim je mozne ten stav nekam jinam predat, zmenit vlastnika, aniz by se narusila jeho konzistence. V FP se stejny problem zvlada tim, ze se ten stav nikam nepredava, ale misto toho se nad nim operuje primo prostrednictvim dodanych pozadavku, jak ho zmenit.
No, asi se to tak trochu da brat, ale v (pure) FP nejde jenom o oddeleni stavu, ale vsech vnejsich akci, od operaci.
Krome toho, nelze to chapat tak, ze to v FP zpusobuje nejaky "chaos" navic, jak to vnima pan Novy - alespon v Haskellu je to co si muzete s danym datovym typem dovolit striktne omezene jeho typem. Skrze typovy system lze skryt i implementacni detaily, samozrejme.
Hm. A jak naimplementujete dohromady identitu a mutabilitu (běžné vlastnosti skutečných entit), když každá změna entity znamená ve FP vytvoření její neidentické kopie? Nebo jinak: Čím je dána identita entity ve FP?
(Čekal jsem, že mi odpoví spíš pan Uživatel si v profilu nezvolil přezdívku.)
Řekl bych, že zdánlivou kopií, předávat se budou de facto jen změny originálu. Například, je-li vstupem číslo 8, implementované to bude (int, [], 8), tak výstup bude po přičtení jedničky (int, [(add, 1)], 8) a výsledek se bude počítat teprve, když už to bude nezbytné, pomocí eval((int, [(add, 1)], 8))
Jde o to, ze by se entita nemela menit. Doporucuju (pokud vas to skutecne zajima) si precist a zamyslet se nad timto claneckem: http://www.clojure.org/about/state Tam se tedy spise mluvi o identitach.
Rich o tom mel nekolik prednasek, zkusim dohledat, jestli bude zajem.
V programování existují podle mě dva přístupy. První je "nějak to uděláme a bude to fungovat" a druhý je "pokud pojedeme podle specifikace a neuděláme logickou chybu, program bude fungovat správně". Reprezentantem prvního (imperativního) přístupu je třeba OOP - autonomní objekty se nějak dohodnou a udělají to dobře. Reprezentantem druhého je třeba čisté FP s tím, že systémy jako Agda a Coq se dokonce snaží (mimo jiné) dosáhnout toho, že správost algoritmu nebo dokonce programu lze prokázat-
Nevýhodou OOP je, že se velmi těžko bezpečně určuje, co daný program dělá, protože je psaný v zásadě metodou "zdola nahoru", postrádá referenční transparentnost, protože je pmlný objektů s měnitelnými stavy, které jsou ke všemu autonomní a dělají si, co "považují" za správné. Z pragmatického pohledu to může být často výhoda, ale principiálně si myslím, že je lepší přístup FP a že vývoj tomu dává pomalu zapravdu.
Teď vůbec nehovořím o problémech s paralelními výpočty, to je podle mě jenom vedlejší efekt.
„...protože je psaný v zásadě metodou "zdola nahoru"...“
Můj ty Tondo... Původní OOP naopak díky pozdní vazbě je vhodné na výstavbu shora dolů a doporučuje ji! To, že to nejde v bastlech typu Ckanál, Java či Chnushnus, je problémem jejich implementace OOP a časné vazby, díky které nejde nekompletní aplikace spustit!
Já chápu, že výpočetní model beze stavů je jednodušší, ale má jednu dost podstatnou chybu - je k hovnu. A objekty (stejně jako funkce) považují za správné jen to, co do nich dáte, FP za vás taky programovat nebude.
Pozdní vazba v tomto smyslu prakticky nic neřeší. Naopak umožňuje původní návrh, který mohl nakrásně být čistý a psaný shora dolů, v průběhu vývojového cyklu hezky rozbít.
FP za nikoho programovat nebude, ale bezestavovou/non-IO část programu lze verifikovat a mít jistotu, co dělá. To je u OOP utopie. A samozřejmě má každý praktický funkcionální jazyk způsob, jak ty čisté části propojit s reálným světem tak, aby to nebylo k h*vnu. Za mě je OOP výrazem rezignace, FP je neustálé hledání cesty k ideálu.
Tyhle příspěvky nepíšu proto, abych se hádal, který přístup je lepší. Ale chtěl jsem napsat svůj pohled na nevýhody OOP; tvářil ses, že Tě to zajímá.
> správost algoritmu nebo dokonce programu lze prokázat
Pro OO jazyky existují nástroje pro verifikaci - např. KeY pro Javu.
> Teď vůbec nehovořím o problémech s paralelními výpočty, to je podle mě jenom vedlejší efekt.
Paradoxně v praxi je s paralelními výpočty ve funkcionálních jazycích často větší problém než v imperativních jazycích.
Viz například už starší zápisek Parallel generic quicksort in Haskell.
On quicksort je ve své podstatě imperativní algoritmus. Funkcionálnímu přístupu daleko víc odpovídají věci typu mergesort. Bohužel se ve funkcionálních jazycích dá jednoduše slepit něco, co trochu jako quicksort vypadá, takže jeden z prvních příkladů bývá tenhleten fujtajxl.
S paralelními výpočty je problém tak nějak obecně. Ve FP jsou ty problémy akorát trochu jiné. Zásadní problém je dělení stavového prostoru na vhodný počet podobně náročných částí. Tohle je často stejně náročný problém, jako to prostě spočítat sériově. A to na paradigmatu nezávisí.
Dávat jako příklad QuickSort je trošku nešťastné. Každopádně jsem psal, že FP hledá cestu tam, kde OOP (IP) rezignuje na vývoj. Že ta rezignace může být někdy pragmatická, nepopírám. S tou Javou je to poměrně dobrý protipříklad, ale Java myslím není zrovna etalon objektovosti podle SB...
Neřekl bych, že rezignuje na vývoj.
Když se podíváte mimo mainstreamové jazyky, tak uvidíte velmi zajímavý vývoj. V kontextu této diskuze je například zajímavý jazyk ParaSail (od lidí z AdaCore). Je to OO jazyk, který je thread-safe (podobně jako Rust) a navíc má implicitní paralelizaci.
Nebo v dnešní době jsou populární aktory a mikroslužby, což jsou vlastně objekty.
Ano, mainstreamové ("OOP") jazyky se vyvíjejí hlavně díky tomu, že si berou (logicky, v OOP paradigmatu není co brát) inspiraci ze světa FP jazyků. A ten Parasail vlastně také (buit-in and inherently parallel map-reduce z příkladu na hlavní stránce jazyka) To je samozřejmě super.
Nevýhoda OOP je v tom, že ak sa začne programátor modliť OOP a nie užávateľovi aplikácie a HW, tak sa často super aplikácia zmrší. OOP fanatik sa viac venuje OOP návrhu, ako odladenosti aplikácie. Kľudne zmrší super fungujúci projekt, či už ho spomalí, alebo zabuguje, len aby bol dokonale objektovo naprogramovaný. Ako už bolo niekde tu v diskusii, alebo v článku spomenuté, vyplodí sa mrte kodu, ktorý spraví to, čo by spravili pipes v konzole na jeden riadok. A čím viac kodu, tým často väčšia chybovosť.
Ja myslim, ze FP lepsi je (viz moje argumentace). Protoze i ten objektovy pristup lze do znacne miry emulovat (treba pres typove tridy, nebo jinak, i kdyz je pravda, ze v jazycich, ktere nejsou tak striktne FP jako Haskell, jdou nektere ty veci hur).
Ale hlavne, je otazka, proc bys chtel mit v programu "entity, ktere neco delaji a dava smysl je o neco pozadat"? (Jak uz jsem psal vys, uz samotna tahle myslenka je zpusob mysleni v OOP.) Treba v Haskellu se tohle da resit pres monady, tedy ze fakticky vracis program, ktery rika, o co se ma pozadat a jak.
To druhe mi prijde vyhodnejsi, protoze nad tim programem muzes nejak pracovat, treba ho interpretovat uplne jinak - jsou to data. Jenom samotna volani ale tezko.
Je to asi trochu jako rozdil mezi GUI a prikazovou radkou. GUI ti nedava moznost napsat skript, ktery neco dela, vsechno se dela primo.
No to záleží, co v tom GUI je. Když vezmu Eclipse IDE tak mi dává jaksi větší možnosti než příkazová řádka.
Ale celá argumentace ohledně FP se točí kolem modulů a jakéhosi volání záhadných funkcí které mají něco spasit, ale pokud chci napsat nějakou funkcionalitu, tak musím nějaký kód vytvořit a jestli ho obalím do metody třídy (OOP) nebo modulu (FP) je úplně jedno. V každém případě budu volat obal::metoda (možná jen jiná syntax) a uvnitř kód: zápis, čtení, inkrementace ............. Fajn. FP je hned nad daty, ale když chci, můžu třeba v Doctrine nebo v Javě Hibernate (mapování dat na objekty) tak k tomu můžu taky lézt přímo a změnou objektu, čí zavoláním jeho funkce pracovat přímo s daty a reflektovat jejich změny.
Tak kde je tedy to jasné zlepšení?
Zlepšení by mohlo být v omezení co s těmi daty můžete dělat, jak je měnit a můžete jen map, filter, reduce. Prosíváte data a na základě vyloveného vrátíte pozměněné kopie.
Na vstupu dostanete adresu kolekce, na výstup uložíte adresu pozměněné virtuální kopie vstupní kolekce. Funkci je jedno z jakou kolekcí pracuje, přístupy k položkám, můžete dodat ve formě funkce, jako parametru.
Místo vytváření abstrakcí ve formě objektů - zákazník, objednávka, vytváříte abstrakce ve formě činnosti - sum_order(type, key, time_range) -> sum_order_items(orders(time_range), id(type, key), name, quantity), kde orders je funkce vracející objednávky, kde id je funkce, která vrací funkci, která otestuje zda aktuální prvek je daná objednávka, další parametry jsou funkce, které vrací funkce, které vrátí požadované hodnoty z řádku objednávky, takže na této úrovni nepotřebujete žádné datové typy a činnost je popsána zcela obecně. Všechny parametry jsou funkce.
Vyhoda FP je prave v te centralizaci pristupu k datum.. (lepe receno asi v tom, ze data jsou rozhrani, jak uz jsem psal) Ze stejneho duvodu se OOP aplikace tak casto svazuji s databazemi, protoze je to proste snazsi (klasicke relacni databaze jsou velmi ne-OOP, jelikoz maji centralni schema). To "distribuovani a skryvani dat" napric mnoha objekty v ramci OOP aplikace je sice jistym resenim modularizace programu, ale ma svoji cenu - vetsi potize pri pristupu k datum. FP ale ukazuje, ze muzeme mit vyhody obojiho - jak modularitu, tak pristup k datum. Toho se dosahuje kombinaci immutability a laziness, jak o tom pise klasicky clanek "Why functional programming matters". I kdyz samozrejme ma to svoji cenu, tou je vyssi abstrakce a vic prace pro kompilator (zatim ne zcela vyresene problemy).
Tak právě ty relační systémy bych jako argument nepoužíval - jsou to externí, neobjektové systémy bez principu zapouzdření, tudíž je třeba počítat s tím, že jestliže jsou jim svěřeny vnitřní stavy objektů, bude možno se k nim dostat. To je ale logické.
(Jak už se tu zmiňovalo) naopak přístup k vnitřním stavům je nežádoucí, protože ohrožuje konzistenci dat. Zapouzdření nevzniklo z pr_de_le!
No ale proc se v OOP programech tak casto pouzivaji relacni, a nikoli objektove, databaze? Neni to proto, ze je to fakticky jednodussi (jak z hlediska databaze tak z hlediska programatora)?
V zasade pokud si odpovite na tuhle otazku, dostanete i (castecnou) odpoved, proc je FP lepsi nez OOP.
Relačné DB sú v prvom rade o zvyku a okrem Caché neviem o žiadnej používanej objektovej DB. Napr. v staršom článku z cz/sk webu bol odkaz na objektovú DB smerujúci na adresu http://www.poet.com/ ...
Možno aplikačný server, alebo jeho admina lahšie odrbeš, že to nie je strata výkonu. Ale databázovy nie. Škálovať aplikačný server je ľahšie, ako škálovať databázový. Aspoň doteraz to tak bolo. Keď data rozdelíš po servroch, niečo/niekto bude musieť rozhodnúť, ktorá časť dat/objektov bude na ktorom servri. V relačnej DB sme už zvyknutý, ako to robiť.
Osobne predpokladám, že z hľadiska "objektového" programátora sú lepšie objektové databázy. Ale z hľadiska HW relačné. Ak by bola RAM DB servra nekonečná, alebo aspoň väčšia, ako DB s ktorou pracujeme, tak je to jedno, aký prístup k datam zvolíme.
Jedno to nebude nikdy, protože přístup do paměti také něco stojí a to, že si člověk může do objektové databáze uložit jakkoliv složitý objekt, znamená, že třeba migrace dat může být dost problémová záležitost.
Jinak existuje jedna objektová databáze, která se docela používá, jmenuje se ZODB. Nad ní je postaveno Zope a nad ním Plone (a různé jeho pluginy). Vydolovat z takové databáze data nebo přejít na vyšší verzi je prý docela fuška.
OK, už mi došlo, že v niektorych implementáciách relačných dat vieš jednoznačne podľa typu tabuľky a dat v nej a polohy jednej informácie polohu druhej. V relačnom svete je to asi jasnejšie. A ďalej načítavanie dat do cache procesora z relačnej, versus z objektovej DB... Takže výtku beriem, ak myslíte toto.
"Relačné DB sú v prvom rade o zvyku a okrem Caché neviem o žiadnej používanej objektovej DB."
Nezapomeňte na AllegroCache. Ale ony se ty objektové databáze asi nedají moc dobře použít, pokud nejsou opravdu těsně provázané s jazykem zbytku aplikace (což AllegroCache je, a jinde jsem nic takového neviděl).
Jinak relační DB se asi používají hlavně proto, že jsou inherentně paralelní.