Přejít k navigační liště

Zdroják » Různé » Jiří Knesl: OOP a funkcionální programování se navzájem vylučují

Jiří Knesl: OOP a funkcionální programování se navzájem vylučují

Články Různé

Už několikrát jsem se setkal s názorem, že je možné psát objektově a funkcionálně naráz, že jsou tyto koncepty vůči sobě ortogonální. Jak si ukážeme, není to pravda.

Tento text se skládá z dvou článků, které původně vyšly na autorově blogu: OOP a funkcionální programování se navzájem vylučujíFP a OOP nejsou slučitelné.

Jiří Knesl už téměř 8 let zlepšuje organizaci práce v IT firmách a IT týmech. Při své práci používá řadu nástrojů, jako je agile, Scrum, Lean, Theory of Constraints a další. Více o něm najdete na www.knesl.com.

OOP a funkcionální programování se navzájem vylučují

Už několikrát jsem se setkal s názorem, že je možné psát objektově a funkcionálně naráz, že jsou tyto koncepty vůči sobě ortogonální. Jak si ukážeme, není to pravda.

Když se podíváme na program, je složen ze dvou částí:

  • algoritmy
  • datové struktury

Nad datovými strukturami programy něco počítají, komunikují atd.

No a teď tu máme 2 principy, které jdou proti sobě. Zapouzdření za OOP a referenční transparence za FP.

Zapouzdření

Objekty si drží svá data. Jiné objekty nesmí přistupovat přímo k datům jiných objektů. Držíme se zásady Tell Don’t Ask a objekty, které data mají, s nimi obvykle pracují.

Typická ukázka objektového kódu:

user.changePassword("newPassword")
user.login("John", "SuperSecretPassword")
user.setName("John")
user.disable()
user.isDisabled()

Objekt mění svůj vnitřní stav, my vůbec nemusíme vědět, jak je uvnitř objektu stav reprezentován, protože komunikujeme výhradně prostřednictvím veřejného rozhraní.

Referenční transparence

Referenční transparence je koncept, který říká, že: „funkce při zavolání se stejnými parametry, musí vždy vrátit stejnou hodnotu“. Doslova můžeme říct, že v místě, kde je funkce volána, můžeme volání této funkce nahradit přímo hodnotou, kterou vrací a nic špatného se nestane (pomiňme teď side-effecty).

Funkcionální kód bude vypadat nějak takto:

userWithNewPassword = changePassword(user, "newPassword")
loginResult = login(user, "John", "SuperSecretPassword")
userWithNewName = setName(user, "John")
disabledUser = disable(user)
disabledResult = isDisabled(user)

Finta není v tom, že user je předávaný jako parametr. I když zavedeme syntax, která umožní pracovat s 1. parametrem funkce jako s objektem (v syntaxi), pořád se lišíme v sémantice.

setName nezmění jméno stávajícího uživatele, ale vrátí jinou datovou strukturu, taky uživatele, který ale má změněné jméno.

Kód:

user = new User(disabled=false)
user.disable()
user.isDisabled()

V objektovém programování vede k tomu, že funkce vrátí true. Ve funkcionálním programování stejný kód vrátí false.

Proč?

Protože user.disable() vrátí nového usera, který už bude disabled. Tento kód by byl správný a vrátil by true:

user = new User(disabled=false)
disabledUser = user.disable()
disabledUser.isDisabled()

Dá se tedy říci, že stačí napsat OOP jazyk, kde budou všechny objekty immutable a takový jazyk se dá použít jako funkcionální?

Ne zcela.

Sdílená data

Mějme situaci, s kterou se objektové programování neumí srovnat a s kterou funkcionální nemá žádný problém.

A to je situace, kdy nějaký algoritmus potřebuje 2 naprosto nespojitelné druhy dat. Třeba objednávku a uživatele pro vygenerování faktury.

order = new Order(basket = [new Item(), new Item2(), new Item3()], deliveryAddress = "Praha")
invoice = order.generateInvoice(new User(name="Jan Novak", email="jan@example.com"))
invoice.saveToFile("invoice.pdf")

V podstatě mě teď OOP nutí vytáhnout data z usera a z items objednávky, protože order pro vytvoření invoice si nevystačí jen se svými údaji. V objektovém programování neřešitelný problém (pokud chceme mít čistě objektový kód). Ve FP žádný problém, data stojí mimo funkce a není porušením ničeho mít výše zmíněný kód.

Výsledek: žádný dostatečně velký program není možné napsat čistě objektově, protože dřív nebo později budete potřebovat porušit zapouzdření.

Side-effecty a funkce bez referenční transparence

I funkcionální programování, pokud má být naprosto čisté, neumí zachytit všechno.

Typickým příkladem jsou funkce se side effecty, např. čtení ze souboru, z databáze, ale i funkce, které pracují s časem nebo vrací náhodnou hodnotu.

Mějme funkci random.

random() -> 0,264567567843
random() -> 0,678328423341
random() -> 0,180939023355

To není funkcionální ani trošku.

Ve funkcionálních jazycích se tyto situace řeší s pomocí monád. Koukněte na kapitolu Purity (a vše pod ní) zde a podívejte se, jak se problém řeší State monádou.

Jenže problém monád je v tom, že ony ve skutečnosti nejsou řešením. Ony jsou jen elegantní cesta, jak do sebe zabalit side effecty a práci s časem tak, aby zbytek kódu mohl být psaný funkcionálně (poskytují interface pure funkcí pro impure efekty). Na úrovni strojového kódu jsou monády implementovány jako imperativní kód (stejně jako vše ostatní).

Žádný čistě funkcionální program nemůže obsahovat žádné side-effekty.

Pustíte ho, on si něco spočítá a pak se ukončí. A nedozvíte se ani výsledek, jednoduše proto, že side effecty nejsou pure, a tak ani výpis není možný udělat.

Závěr: V reálu pak platí, že v hypotetickém čistě funkcionálním jazyce není možné napsat žádný užitečný program.

Někteří vývojáři ve funkcionálních jazycích tvrdí, že mají 90 % kódu čistý funkcionální kód a side effecty mají stranou. Nevím, funkce, které píšu já, obsahují v 80 % případů side effecty a nejsou tedy čisté funkcionální programování. A to nepoužívám side-effekty, globální stav atd. nikde, kde to není nezbytně nutné.

Závěr

Ukázali jsme si nejen to, že není možné psát souběžně čistě objektově a čistě funkcionálně. Ukázali jsme si i to, že v praxi nemůžeme psát jen samotně čistě objektově (že o něčem prohlásíme „vše je objekt“ je bezvýznamné tvrzení, které samo o sobě nestačí k prohlášení jazyka za čistě objektový) nebo samotně čistě funkcionálně (dokud existují side-effecty, což bude vždy).

A v praxi, když už budeme psát program, vždy si musíme vybrat, jaké je naše dominantní paradigma a druhé paradigma můžeme použít jako nádech našeho programu (obvykle objektový kód s nádechem funkcionálního, jako má např. Scala). Když dojde na lámání chleba, musíme zvolit, jestli zapouzdříme nebo zachováme referenční transparenci.

users.map(fn [user] {if (user.loggedInLastWeek()) user.setActive(true); else user;})

Je kód objektový s nádechem funkcionálního. A může být víc funkcionální (pokud vrátí kolekci nových uživatelů), ale nejčastěji bude objektový (mutuje existující uživatele).

To se zdá jako malý rozdíl, ale v praxi mezi:

users.map(fn [user] {if (user.loggedInLastWeek()) user.setActive(true); else user;})

a

usersWithActivatedUsers = users.map(fn [user] {if (user.loggedInLastWeek()) user.setActive(true); else user;})

je obrovský rozdíl ve výsledném kódu a architektuře. Zkuste si to a uvidíte.


FP a OOP nejsou slučitelné

Mám pocit, že nedorozumění pořád pokračuje. Pokusím se je znovu vyvrátit.

Jádrem je, že si lidé myslí, že když můžou nad objektem zavolat map, nebo filter, že to je TO funkcionální programování.

Ale ono není.

Tak například příslibem objektového programování je to, že mám objekty, které jsou zaměnitelné, samostatně funkční a mělo by stačit znát jejich public interface k tomu, aby je vývojář uměl použít. O další věci (jako vnitřní stav objektů) se starat nemusí.

Důsledkem toho, že se vystavuje chování, je fakt, že se stav rozdrobuje do třeba stovek objektů.

Funkcionální programování takhle nefunguje. Tam platí jednoznačně to, že vývojáři vytlačují stav ven a koncentrují ho v minimálním počtu míst. Je běžné, že některé služby jsou zcela bezstavové, nebo uchovávají stav v jedné proměnné.

Vytáhnout stav ven z objektů je dokonale antiobjektové chování.

Další důležitou součástí funkcionálního programování jsou důsledky snahy dělat pure funkce.

V pure FP platí, že když zaměním zavolání funkce jejím výsledkem, nic se nestane. I v ne-pure jazycích vývojáři svůj kód organizují tak, aby toto platilo v co největším kusu aplikace.

Je to jeden z důvodů, proč se často říká, že v FP není potřeba Dependency Injection. Funkce je funkce. Matematika a tak. Prostě proč bych si injectoval (pro potřeby testování) vlastní funkci plus? A jako je funkce plus závislá pouze na svých vstupech, tak má naprosto konzistentní a předvidatelné výsledky. To, že ji nevymockuju, mi při vývoji nedělá žádný problém. Není to ale úplná pravda a někdy se DI hodí, ale naštěstí nebývá problém udělat funkci High Order Funkcí a to skutečně potřebné si předávat.

Proti tomu v OOP máme koncept toho, že pošleme objektu zprávu. A objekt je chytrá černá skříňka, která nějak zareaguje. Může se změnit. Může něco vrátit. A pokaždé může vrátit něco jiného. Taky může třeba vteřinu počkat a pak něco vrátit.

Zasílání zpráv, kdy nevíme, co se pak stane a jestli se nám něco vrátí versus zavolání funkce zkrátka není totéž. Mnoho jazyků má podobnou syntaxi pro definici funkce a metody, ale mentálně se jedná o velmi odlišnou věc.

Funkce nezná žádné okolo (ignorujeme-li věci jako globální proměnné). Dostane parametry a vždy vrátí totéž. To není jen věc FP, tohle je slušnost i v Pascalu.

Metoda nějak pracuje s jinými objekty, s atributy a způsob, jak může zareagovat, je mnohem širší.

Ano, i Erlang je v podstatě objektový jazyk. Má actory, kteří si zasílají zprávy. Ale právě ta část kolem actorů a posílání zpráv nemá zase vůbec nic společného s FP. V FP píšete funkce (a pak je ten kód funkcionální) nebo actory (a pak je objektový). Actor, ač sdílí syntaxi s funkcí, není funkce. Je to obvykle rekurzivní stavový automat závislý nejen na parametrech, které si předává, ale i na stavu dalších actorů (obvykle dalších rekurzivních stavových automatů) a na tom, jak „příroda“ seřadí příchozí zprávy na mailboxu daného actora. Tohle fakt nemá s FP nic společného.

V určitou chvíli si vývojář musí vybrat, kterou cestou se vydá.

Buď bude kód rozbíjet do mnoha malých krabiček, které se o sebe postarají samy a navzájem si posílají zprávy. To nemůže nikdo považovat za FP kód.

Nebo se stavu téměř zbaví, dostane do jednoho místa to, co ze stavu zbylo a kolem toho vytvoří aparát funkcí, které jsou referenčně transparentní a jen minimum funkcí má nějaké side effecty. To znamená mnohdy porušovat zapouzdření, mainstream OOP je určitě úplně jinde a ikdyž to takhle někdo zkusí udělat, tak mu do toho budou zasahovat stažené knihovny, které to budou porušovat (smutná realita např. světa JS, kdy vy sice můžete uzavřít stav do mori, ale stejně skoro vše, co si z internetu stáhnete, si bude udržovat stav v objektech).

Komentáře

Subscribe
Upozornit na
guest
56 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
tacoberu

Hodně výstižná mi přijde věta: „není možné psát souběžně čistě objektově a čistě funkcionálně.“

Dovolím si ji dovést do extrému: Nejde psát čistě funkcionálně nikdy nikde. V žádném jazyce. Ani v Haskellu protože Monády. A psát čistě objektově sice jde, ale je to hrozně nepraktické, pracné, a nikdo to stejně neumí.

Já jsem zvolil způsob, kdy píšu co nejvíc funkcionálně to jde, v jazyku který jsem nucen používat. Plus jsem si z OOP vzal ty nejužitečnější koncepty: api, zastupitelnost a zapouzdření (plus určitě i něco dalšího).

No, a spousta věcí vůbec s OOP ani s FP nesouvisí, jak si sám napsal: „Funkce nezná žádné okolo (ignorujeme-li věci jako globální proměnné). Dostane parametry a vždy vrátí totéž. To není jen věc FP, tohle je slušnost i v Pascalu.“.

Jiří Knesl

„Nejde psát čistě funkcionálně nikdy nikde.“ to záleží, jak vnímáš pure FP. Jestli nemáš opravdu žádné side effecty, mutable stav apod. (pak by žádný program fakt napsat nešlo, nebo šlo, ale nedostal by se z něj žádný výsledek), nebo jestli je to nemít ve svém programu. Tzn. pokud oddělíš ne-pure-FP runtime a tvůj pure-FP program, který ten runtime využívá, pak soudím, že je možné psát čistě funkcionálně.

Atamiri

Monády ale čistě funkcionální jsou, alespoň v teoretické rovině, takže pokud se bavíme o konceptu a pomineme konkrétní implementace, čistě funkcionálně psát jde.

v6ak

Zrovna u Scaly bych rekl, ze dominuje spise funkcionalni paradigma. Bylo by to krasne videt napriklad na Play frameworku. Doporucuje se pristup: Dokud muzes, bud radsi funkcionalni, ale nepouzivej monady apod. jen abys zustal funkcionalni. A ostatne v clanku uvedene pouziti metody (ano, metody…) map taky napovida, ze bude dominovat fuincionalni paradigma. Ta imperativni verze mi ve Scale prijde dost exoticka.

Jako typicka ukazka objektoveho pristupu s nadechem funkcionalniho mi prijde Ruby s Rails.

(Samozrejme, muzu kterykoli z techto jazyku vzit a pouzit jinak, nez je mainstream.)

(BTW, davat takove side effects do metody map je hnus. Kdyz uz musim, tak ne map, ale foreach.)

Jiří Knesl

Nemám přehled o tom, jak vypadá mainstreamové použití Scaly, ale z meetupů s Scalisty mi přišlo, že jsou to objektoví vývojáři, kteří používají zatím Scalu jako lepší Javu a postupně nakukují i do světa FP. Ale jsou výjimky a najdou se Scalisti, kteří ten jazyk používají podobně jako Haskell, jen já je osobně nepotkal.

U toho zdrojáku jsem musel použít map, protože ty usery vracím a mám novou kolekci.

v6ak

Byl jsem na pár (asi dvou) meetupech v Brně a mám pocit, že relativně velká část byli nováčci, kteří šli z Javy. Takže přirozeně budou používat přístup „lepší Java“. (No offense, taky jsem tak začínal.) Plus tu mohlo být pár typicky imperativních témat jako actory.

Já mám přehled spíš o tom, jak píšou kód autoři různých knihoven (včetně Play frameworku, který je dnes pod LightBendem) a jak ho píšu já. V Play 2 si neušpiníš ruce imperativním kódem, pokud nemusíš (typicky databáze), ledaže bys chtěl. U asynchronního přístupu (např. reactive streams) je tu taky snaha být maximálně deklarativní.

Z toho všeho mi Scala přijde spíš jako „Haskell bez monád“ než jako „lepší Java“. Akorát asi hodnotíme podle jiného vzorku.

Z tomu zdrojáku: Sice mám novou kolekci, ale v čem se bude lišit od té předchozí?

v6ak

Zminene vygenerovani faktury IMHO lze udelat ciste objektove. Proste objektu tridy User reknu, at mi kousek te faktury vykresli. A on mi vykresli veci jako adresu apod. na zadane misto.

Je to samozrejme poruseni MVC. Je tu samozrejme spousta duvodu, proc to muze byt neprakticke delat takto. Ale pokud se nahodou chceme drzet skriktne OOP (za cenu ustupku jinde), moznost tu je.

Jiří Knesl

Ano. Mnohokrát jsem nad tím uvažoval. V MVC si umím představit řešení jako „předávání šablony modelu“, tedy obrácení toho, jak se to obvykle řeší. Model pak řekne: „Dej mi render pro tato data.“ Taková implementace by ale byla hodně nestandardní a asi i dost nepraktická.

x86

Může být praktická, standardní, elegantní i užitečná – viz třeba QPainter a renderování do Svg v Qt.

v6ak

„Na úrovni strojového kódu jsou monády implementovány jako imperativní kód (stejně jako vše ostatní).“

Pri argumentaci ohledne funkcionalniho pristupu nabizenymi komponentami jazyka radsi argumentuju vnejsim chovanim, treba absenci side effectu nebo lepe nejakou urovni referencni transparentnosti. Jinak, jak ostatne i trosku naznacujes, nebude ciste funkcionalni ani scitani Integeru, protoze z

v6ak

043=_

v6ak

Predcasne odeslano, sorry.

Jinak, jak ostatne i trosku naznacujes, nebude ciste funkcionalni ani scitani Integeru, protoze za nim je nejaka imperativni instrukce procesoru.

Ve skutecnosti to ani s tou referencni transparentnosti neni az tak jednoduche. Vezmeme si priklad String.toLowerCase() v Jave. Je to referencne transparentni?

  • Ne, protoze uvedenou substituci zmenime delku vypoctu. S takovou bychom ale nemeli nic referencne transparentniho, takze tuto vlastnost promineme.
  • Ne, protoze poprve nam to treba projde a podruhe hodi OutOfMemoryError. S timto se da poprat ruzne, ale asi tez budeme muset trosku couvnout. (Da se pak ale diskutovat napr. o tom, zda muzeme chytat OutOfMemoryError.)
  • Ne, protoze dve ruzna volani vrati dve neidenticke (byt ekvivalentni) instance Stringu. Toto by se dalo vyresit (napr. zakazat test identity), ale ja se radsim spokojim s oslabenim definice referencni transparentnosti.

Podobne komplikovane to je i u side effects.

Ondřej Novák

Srovnávají se hrušky s jabkama. Mohu používat OOP a přitom funkcionální styl programování. Stačí, když důsledně budu používat immutable objekty. On totiž, kdo napíše, že OOP se vylučuje s funkcionálním programování jen ukazuje, že nepochopil OOP. Sorry člověče.

Ale co mi to ta funkce ve funkcionálním programování vrací? Objekt ne? On i základní typ je objekt. Může mít metody, dědičnost, polymorfismus. Místo aby měnila svůj vnitřní stav, může fungovat jako továrna.

User disabledUser = user.createDisabledUser()

Je to krásná teorie, a rozhodně se tohle hodí mít zavedeno u multithreadingu, protože odpadá většina synchronizace. I na změny sdílených immutable objektů mám cosi, čemu říkám future-stream, každý objekt kromě svého aktuálního stavu poskytuje futuru na svou budoucí změnu. Změna objektu znamená nastavení futury a každý, kdo objekt sleduje se ihned dozví, že už existuje nová verze.

Ale jako vždy je to všechno o praktickém využití. Mít megový JSON dokument a chtít ve sedmý úrovni zanoření změnit jedno číslo abych dostal novou verzi megovýho JSONu… to rozhodně rychlosti nepřidá. Pro hromadné změny potřebujeme transakce … ehm… objekty (RIAA, atd)

tacoberu

On totiž, kdo napíše, že OOP se vylučuje s funkcionálním programování
jen ukazuje, že nepochopil OOP. Sorry člověče.

OK. Můžeš mi prosím vysvětlit jak pošlu zprávu nějakému objektu, kterýžto mě informuje o svém výsledku také pomocí zprávy – ale tak, aby nedocházelo k side-effectům? Přirozeně očekávám, že to bude asynchronní. Protože takto je definováno pure OOP.

To, že nějaká funkce vrací objekt není OOP, to je jen slovíčkaření.

Ondřej Novák

Mně fascinue, jak se vždycky teoretici při kritice oboru, kterému nerozumí zabřednou v technikáliích. „Asynchroně“ nemá s OOP nic společného. Poslání zprávy může být klidně realizováno i voláním metody. Nijak to smysl OOP nenarušuje.

Prostě „poslat zprávu“ je jen pojem vyjadřující nějakou interakci, aniž by popisovalo, jak ta interakce je realizována.

Nevím, jaké side-efekty máte na mysli.

tacoberu

Mě fascinuje věcí.

kód: 1 + 1 není OOP, je FP.
kód: m += 1 není FP, ale je OOP.

Je jasné proč. A je jasné, proč to nejde skloubit. Cokoliv mimo to nesouvisí s OOP ani s FP.

Na asynchronnosti je zajímavé, že takto byl OOP původně navržen. A že ji do FP nenaroubuješ ani kdyby si se na hlavu postavil.

Ondřej Novák

Já to asi už chápu. Vy prostě synchronnímu volání říkáte funkcionální programován, a asynchronímu volání říkáte OOP. Že se to neslučuje asi celkem jasné. Ale já s tím prostě nesouhlasím. OOP není jen o asynchroním posílání zpráv. Já tohle chápu jen zobecnění nějaké představy.

Jako objekt v reálném životě mohu na úřad poslat dopis se žádostí a čekat na odpověď. Nebo mohu na úřad dojít a počkat si u okénka. Jedno je poslání zprávy, druhé volání funkce (synchronní zpráva). Poznávací znamení? – to první vyžaduje, aby příjemce věděl, kam má poslat odpověď. To druhé nic takové nepotřebuje, úřednice ví, že má odpovědět tomu, kdo zrovna stojí u okénka.

1+1 je taky OOP. 1.operator+(1). Uřednice úřadu 1 u okénka operator+ mi sdělí, že výsledkem požadavku 1 je 2.

Taco

1+1 je taky OOP. 1.operator+(1). Uřednice úřadu 1 u okénka operator+ mi sdělí, že výsledkem požadavku 1 je 2.

A podle jaké definice je toto objektové?

1 + 1 je ve skutečnosti, že si zajdu k úřednici úřadu 1, řeknu: „s dovolením si půjčím tady jedno +“ a předhodím mu jinej úřad? To je podle vás objektové?

Jako jedno se vám musí uznat. Odpovídá to dnešnímu chápání většiny OOP vývojářů. Bohužel, ale ano.

falken

to je fakt, a čím větší hlouposti, tím větší expert (s vyjímkami potvrzujícími pravidlo); váš popis je přesně to co o tom jenom tuším já, i když jsem to v posledni dobe zatím ani nepotřeboval (jo, kdysi kolem 1990 jeden DOS file manager ve stylu nortonu, ArcShell se to jmenovalo, umel psat shell bat-skripty s promennymi ktere se spoustely nad mnozinou souboru/adresaru vyselektovanych pres UI (map?) – to bylo defacto funkcionalni pouziti uz tenkrat, az na to, ze tomu tak tenkrat nikdo nerikal – a bylo to naprosto super, tak super, ze jsem to pak par let hledal nekde ve windows, doufaje ze TotalCommander treba nekdy embeduje LUA a pujdou takovehle veci skriptovat, fakt rychle, ad hoc, „jako tenkjat“ … nikoliv, nestalo se; a vubec jakekoliv volani po ultra cistote mi taky evokuje nejake fasizovani, takze pryc od toho…)

Jiří Knesl

Martin Takáč (tacoberu) nejspíš odkazuje na koncept OOP, kdy se předpokládalo, že všechny objekty budou „living“, tedy souběžně běžící a komunikující prostřednictvím zpráv. Je vidět, že oba máme nastudováno víc než vy. Ale bez urážky, v ad hominem si nelibuji.

Můžete si nastudovat některý výroky Alana Kaye (autora OOP) zde: http://mythz.servicestack.net/blog/2013/02/27/the-deep-insights-of-alan-kay/

Zajímavá je část: „To me, one of the nice things about the semantics of real objects“, kde své vnímání naznačuje. Můžete se pokusit jeho názory vyvrátit, ale nepředpokládám, že rozumíte OOP víc než jeho autor.

Poslání zprávy je potom operace, kdy objektu přichází na dispatch message a on rozhoduje, jak s ní naloží. Nemusí vůbec nic vrátit. Někteří vývojáři razí názor, že Actor model je právě ztělesněním OOP, jazyky jako Erlang jsou v tomto pohledu (ne-pure) OOP.

To je přístup, který je naprosto nepochybně plný side-effectů.

balki

Alan Key nie je autor oop, ale autor smalltalku.

balki

sorry, zvrzal som my meno.

Jiří Knesl

Celé je to složitější. Hoare přišel s record classami. Nic, co by ale nějaký vývojář vzal a používal to, teoretický koncept. Nydahl, Nyygard to vzali, nacpali do Simuly a vytvořili první jazyk s objektovými rysy. Jazyk pořád nebyl pure (vše je objekt). Pak Kay, Ingalls a Goldberg udělali Smalltalk, první pure-OOP jazyk (vše je objekt). Nemám problém kohokoliv z těchto lidí označit za autora OOP. Kay mi tam jako teoretik OOP přijde určitě nejvýraznější.

balki

Presne, tak to je.

Jiří Knesl

Článek není o tom, že můžu používat OOP a funkcionální styl. Článek mluví o tom, že když jedno doženete do krajnosti, druhé se stane nemožným.

Co se immutable objektů týká, pořád zůstává problém s identitou. Nám 2 samostatně vytvořené instance StringBufferu sb1, sb2, v obou string „Hello World“. Má platit sb1 == sb2? V objektových jazycích se vám toto vyhodnotí jako false, ve funkcionálních jako true.

Ad datové struktury – např. JSON) nemusím používat copy on write přístup, existují persistentní datové struktury. Nejsou sice tak efektivní, jako mutable, ale jsou mnohem lepší než copy on write přístup.

Ondřej Novák

berete operátor == jako dotaz na to, zda jde o stejný objekt, nebo zda dva objekty jsou stejné?

Jde o různé objekty, které vypadají stejně. Já a moje dvojče jsou stejné objekty, ale každý je samostatná entita.

Také záleží, jak je zavedena identita objektu. Pokud jako ID dám SHA256 obsahu, pak není možné vytvořit dva objekty se stejným obsahem.

Jiný příklad. a=new koule(červená), b=new koule(červená), (*a==*b) == true, (a==b) == false
jsou to dva objekty, ale jsou stejné.

Hraje to roli? Jirka a Jarda jsou dvojčata, oba vědí/umí zařídit totéž. Je tedy jedno, zda se zeptám Jirky nebo Jardy.

Jiří Knesl

Beru to tak, že FP jazyk zná jen hodnoty, ne identity. V OOP tuto otázku musíte zodpovědět.

x = new Koule(červená)
y = new Koule(červená)

x == y – má být true nebo false? IMHO v OOP false, v FP true.

x.setBarva(modrá)

x == y – teď má být false v obojím

A samozřejmě se mi může stát, že budu chtít vytvořit více objektů se stejnou barvou. Třeba když budu simulovat kulečník (karambol, snooker), kde mám koule stejných barev, ale každá je samostatná, má svou identitu. V mutable světě může objekt projít situací, kdy se hodnoty objektů rovnají a pak zase divergují. V FP světě je hodnota stálá a nezávislá na čase, byla tady vždy, ještě před počátkem vesmíru.

Další důležitý aspekt, byť je to spíš o výkladu, než o jakési vědecké slučitelnosti, jsou zvyklosti vývojářů v OOP a FP světě. OOP vývojář usiluje o autonomní komponenty, které dovedou samostatně fungovat. Postarají se o sebe včetně stavu. Oddělit stav od dat by bylo považováno za projev špatného stylu (terminologií anemický model na jedné straně, feature envy na druhé). V FP je to naopak preferovaná cesta, kdy není výjimkou, že se píšou aplikace, kde celý stav aplikace napříč všemi komponentami je v 1 proměnné.

Ondřej Novák

A kde jste definoval že operace == je identita objektů?

Vyrobil jsem si dvě koule, obě jsou červené. FP mi tedy dává špatný výsledek, pokud se ptám, zda jde o stejnou kouli. Pokud se ale ptám na to, zda koule stejně vypadá, pak je to OK. Tohle totiž vůbec nesouvisí se stylem programování ale s definicí toho, co chcete.

setBarva – vezmu tu kouli a nabarvím ji na modro. Ve FP to znamená, že musím vyrobit novou kouli. Opět záleží na tom, co chci dělat, zda chci koule přebarvovat nebo klonovat přebarvené. OOP má hlavně odraz v reálném životě. Snaží se napodobovat tak, jak to ve světě funguje. FP se naopak snaží chovat jako matematika. Nicméně všichni v reálném světě matematiku používáme a nevylučuje se.

Ale nikdo, opakuji, NIKDO by neměl tvrdit, že OOP neumí porovnat objekty, nebo že zprávy se musí posílat asynchroně. Ten kdo to tvrdí ve skutečnosti neví nic o objektovém programování. Možná mluví jen o úzké implementaci, třeba o smalltalku, nebo o Javě. Ale takhle já OOP nechápu a odmítám chápat. Je to totiž mnohem širší pojem, než jen omezení se na pár „pěkných“ nápadu ze smalltalku. A je mi naprosto jedno, jestli ten pohled prosazuje nějaký pan Knesl, pan Kay, nebo Bůh. Není to svaté písmo. Možná že pan Kay našel určitý směr myšlení, ale ta myšlenka se dostala mnohem dál než on sám.

Jiří Knesl

„A kde jste definoval že operace == je identita objektů?“

Prakticky ve všech objektových jazycích to tak je. Napadá vás nějaká výjimka?

FP nedává špatný výsledek. Ono neříká, že jsou „jedna koule“, ono říká, že ty koule jsou stejné. Když chci změnit hodnotu, nemusí vytvářet novou kouli, pokud použiju persistentní datovou strukturu. Ta je sice náročnější než mutable stav, ale není to copy on write.

To, že OOP má odraz v reálném životě, je těžce nadužívané tvrzení. Máte pro toto tvrzení nějaké opodstatnění?

Netvrdím, že OOP neumí porovnat objekty. Jen tvrdím, že OOP cílí na jiné věci, než FP a když se jedno dožene do extrému, popře se tím to druhé. OOP vytváří izolované jednotky, které v sobě obsahují jak stav, tak chování. FP tyto věci odděluje. OOP se snaží o zapouzření, FP nikoliv. Kód, který bude dobrý v OOP bude porušovat principy FP a naopak.

Co se vašeho chápání OOP týká, je mi to jedno. Přidržím se toho, jak ho vnímá autor OOP. Je pro mě autorita. Vás jsem v životě neviděl, netuším, co vlastně o programování víte, četl jsem pouze vaše komentáře (s kterými nesouhlasím a které autora OOP popírají) takže se na mě nemůžete zlobit, když řeknu, že vaše vnímání OOP je zkomolené a nesprávné.

Ondřej Novák

Poslední věta mě v zásadě utvrzuje v názoru, že nerozumíte OOP. Osobně si myslím, že mu nerozumí ani jeho Autor. Osobně si nemyslím, že by nějaký autor OOP vůbec kdy existoval. Mě OOP učili na UHK v předmětu Objektové Modelování (a už to nějaký pátek bude). OOP jako způsob myšlení v softwarovém inženýrství je úplně něco jiného, než prezentuje jeho „autor“.

„To, že OOP má odraz v reálném životě, je těžce nadužívané tvrzení. Máte pro toto tvrzení nějaké opodstatnění?“

Samozřejmě. Tak jako svět se skládá ze samostatně fungujících entit, tak i OOP se skládá se samostatně fungujících objektů, které spolu spolupracují. Ale neznamená to, že objekty musí žít nebo mít stav. Červená koule je jen jedinečná instance, vznikne a už se nemění. Její poloha nemusí být uložena v jejím stavu (je poloha stavem koule?, podle mě nikoliv). I neživé koule se můžete ptát (jakou máš barvu?) Hlavním smyslem OOP je ale uvědomění si, že data a operace nad nimi patří k sobě. Neznamená to ovšem, že by ty data nebyly přístupné z venku, ani to, že se musí vnitřní stav měnit.

Hlavním přínosem OOP je jednotný definice interfaců. K udržení většího projektu je třeba nadefinovat jak jednotlivé části komunikují a to lze právě pomocí interfaců. Interface pokrývá kód i data a tvoří tak objekt. Objekty v tomhle tvaru lze najít už v prvních unixech, vlastně celý shell je o objektech, kdy objektem je process, rozhraním je příkazová řádka a standardní vstup a výstup (a chybový výstup). Zobecnění toho přístupu vede na k use case diagramů, kde začíná jasné oddělení rolí a komponent a rozhraní mezi nimi.

Pokud na FP nahlížíte jako na způsob návrhu algoritmů, kdy mám data plně oddělena od kódu (kdy se dostáváme k relačním databázím, k E-R schámatům a zpátky do minulého století), pak v tomto směru toto vidím jako slepou větev. Pokud ale dál v FP čerpáte výhody OOP (například javascript sice má na všechno funkci, a lze ta programovat funkcionálně, ale beztak si tam nakonec začnete předávat objeky), pak jste jen vzal OOP a omezil jste ho na immutable objekty a volání metod. Nemám s tím problém, ale je to sorry, pořád OOP, jen se jinak na něj díváte

Takže a = fn(b,c) je ekvivalentni k a = b.fn(c).

Takhle se programovalo za mě v době, kdy C++ byla vyšší dívčí a kdy jsem jsem ještě stále zůstával v ANSI C a snažil se nějak objekty simulovat, protože jsem je prostě potřeboval (aniž jsem si to uvědomoval)

tacoberu

Hlavním přínosem OOP je jednotný definice interfaců. K udržení většího projektu je třeba nadefinovat jak jednotlivé části komunikují a to lze právě pomocí interfaců. Interface pokrývá kód i data a tvoří tak objekt

Podle této definice je Haskell OOP. Kurnik, já si to myslel! :)

Jirka Kosek

Proč by x == y mělo ve funkcionálním jazyce vracet true? To přece záleží na tom, jak si nadefinuji datový model a identitu nad jeho prvky.

Jsou x a y identické? Nejsou. Mají stejný obsah? Mají. Ve spoustě jazyků máte odlišné operátory/funkce pro porovnávání identity a samotného obsahu.

Např. v XSLT, což je funkcionální jazyk, můžete testovat buď identitu

$x is $y

nebo jen shodu obsahu:

deep-equals($x, $y)
Jiří Knesl

Vrací true proto, že func.jazyky žádné identity neřeší. Řeší jen hodnoty. Pokud se v nějakém pojmenovaném kousku paměti (který se v imperativním programování nazývá proměnná) objeví stejná data jako v druhém, tak záleží jen na těch datech.

Ano, může se stát, že data reprezentovaná stejnými bity, ale jiného typu, budou v paměti stejná, ale na úrovni typu ne, protože jedno bude char[] a druhé string a jazyk v tomto bude vidět rozdíl. To je ale jiná situace.

V okamžiku, kdy mám stejný typ a stejnou hodnotu, tak si troufnu říct, že není FP jazyk, kde by a == b bylo false. Opak platí pro objektové jazyky. Třeba se mýlím v tvrzení toho, že nejsou výjimky, ale v jazycích, které používá 99 % vývojářské populace, to tak je.

jirkakosek

Troufnout si můžete, ale mýlíte se. Troufám si odhadnout, že nejpoužívanější funkcionální jazyk je dnes XSLT, a tam zrovna tohle neplatí.

v6ak

Obecně identita nemá smysl v pure FP. Dokonce by nám rušila referenční integritu, pokud ji chceme chápat extrémně.

Ekvivalence má smysl u FP i OOP, byť u FP je víc v popředí. Java má Object.equals(Object), má tedy taky ekvivalenci. A – ačkoli to kontrakt java.lang.Object striktně nevyžaduje – chová se typicky funkcionálně, zejména u immutable objektů, jako třeba java.lang.String.

Mimochodem, tady je Scala spíše funkcionální. Operace == ekvivalence a ne identita. Možnost testovat identitu ve Scale sice taky je (jako i v dalších non-pure funkcionálních jazycích), ale je „skrytá“ pod eq a nepamatuju si, kdy jsem to napodledy použil (popř. viděl). Snad na nějakou mikrooptimalizaci. Narodíl od Javy se snaží Scala ekvivalenci i propagovat, protože na case classes defaultně funguje typicky funkcionálnálně, zatímco v Javě musím ručně psát equals a hashCode. A to ještě nepočítám problémy s null.equals v Javě.

tacoberu

Nejsem si jist, jak to s tou identitou Jirka myslel, ale:

V OOP je identita dost důležitá, protože ty můžeš vytvořit dva zcela ekvivalentní objekty. U druhého si zaregistruješ posluchače. A když pošleš zprávu prvému objektu tak nepřekvapivě informaci o změně prvního objektu nedostaneš. Takže v OOP je kardinální rozdíl mezi identitou a ekvivalencí.

A ve FP? Vytvoříš dva objekty. Zaregistruješ si u druhého posluchače = máš tři objekty. Prvnímu pošleš zprávu = už máš čtyři. A takhle to jede dál… dokavad nepochopí člověk, že se to v FP musí dělat jinak. Na identitu či ekvivalenci si ani nevzpomeneš.

Palo

To co pisete o OOP je blbost. Ked chcem napisem to tak ze budete dostavat hlasenie o zmenach aj z druheho objektu ak sa zaregistrujete na prvom. Problem je ze obvykle to tak NECHCEM a preto sa to OBVYKLE nepouziva, nie preto ze by sa to nedalo.

tacoberu

Zdá se mi, že jste si nepřečetl co jsem napsal. Dejte se prosím pozor, ať neargumentuje proti něčemu, co jsem neřekl.

Ondřej Novák

Psal jsem komentář a nějak se ztratil, nevim

ale napsal jsem něco ve smyslu, že je to srovnávání hrušek a jablek. Zkrátím to. Co vrací funkce ve funkcionálním programování? Jakého typu je hodnota? Může to být objekt? Proč by ten objekt neměl používat vlastnosti OOP?

balki

Z clankoch o paradigmach (a o navrhovych vzoroch) na zdrojaku sa mi vzdy zjezja vlasy. Rychlokvaseni odbornici na vsetko maju jasne nazory, miesto toho aby si nieco precitali :(

Oldis

Se vylučují v pure formě, ale spolu fungují velmi dobře ;)

Ondřej Novák

Pak je to ovšem jen akademická diskuze. :)

Oldis

No tak kdyz zduraznuje pure formu v clanku tak uz clanek je akademicka diskuze, praxe je jina.

Pavel Strnad

Clanek je bohuzel velmi zavadejici a pouze ukazuje castecne znalosti/neznalosti autora. Pokud autor neco tvrdi, pak je dobre tvrzeni oprit o nejaky dukaz, pripadne citaci. Doporucuji autorovi si o monadach neco precist a prosim neplest dve veci teorii a implementaci v konkretnim jazyku. Jako vychozi material doporucuji (http://homepages.inf.ed.ac.uk/wadler/papers/marktoberdorf/baastad.pdf).
Jinak ocenuji odvahu autora neco takoveho sepsat :-)

Jiří Knesl

Která konkrétní tvrzení chcete dokázat?

Je pravda, že roky programuju v ne-pure FP jazyce. Monády chápu pouze do určité úrovně. To uznávám.

David Fogaš

Když se podíváme na program, je složen ze dvou částí:
algoritmy
datové struktury
Nad datovými strukturami programy něco počítají, komunikují atd.

Hm, z výše uvedeného mi vyplývá, že program provádí operace s nějakým podprogramem a sám o sobě může být předmětěm manipulace ze strany programu mu nadřazenému. Pochopil jsem to správně? Jestli ano tak mi to přijde nešikovně napsané.

Jiří Knesl

Bylo to myšleno tak, že mám data, která nejsou executable a algoritmy. Ty algoritmy berou vstupy, data a počítají a pracují s těmi daty.

Radek Miček

1) Chybí mi tu přesná definice, co autor považuje za OOP a funkcionální programování. Například je to, co dělám v beztypovém lambda kalkulu (se záznamy) funkcionální programování? Co když pak nad tímto lambda kalkulem vybuduji jazyk s objekty?

Referenční transparence je koncept, který říká, že: „funkce při zavolání se stejnými parametry, musí vždy vrátit stejnou hodnotu“.

2) BTW tahle definice příliš dobře nefunguje v jazyce s vedlejšími efekty. Například mohu mít klasicky definovaný map, ale když mu jako funkci, pomocí níž se mapuje, dám rand, tak podle té vaší definice nebude referenčně transparentní (i když normálně bychom ho za referenčně transparentní považovali).

3) Píšete

Nevím, funkce, které píšu já, obsahují v 80 % případů side effecty a nejsou tedy čisté funkcionální programování.

ale podle

Na úrovni strojového kódu jsou monády implementovány jako imperativní kód (stejně jako vše ostatní).

jenž je psáno o kousek výš, byste měl side effecty všude (ne jen v 80 % případů). IMO lepší by bylo do toho vůbec netahat strojový kód a soustředit se například jen na operační sémantiku jazyka.

4) Ještě jednou k:

Někteří vývojáři ve funkcionálních jazycích tvrdí, že mají 90 % kódu čistý funkcionální kód a side effecty mají stranou. Nevím, funkce, které píšu já, obsahují v 80 % případů side effecty a nejsou tedy čisté funkcionální programování.

Funkce s vedlejšími efekty lze přesunout do nějakého interpretru – tj. váš hlavní program pouze řekne, že se má zavolat například println a co se má dělat po zavolání – tj. celý program kromě interpretru je bez vedlejších efektů.

V Haskellu je pak interpretr pro IO mimo program – tj. program je bez vedlejších efektů (pokud nepoužijete FFI, unsafePerformIO apod.).

Radek Miček

tj. program je bez vedlejších efektů

To je špatně – program v Haskellu může divergovat, což je efekt.

čumil

Chlape, furt strašíš s tím interpretováním operací dělajících side efekty ?

Sakra, uvědom si, že podle týdle jebnutý logiky je funkcionální (bez side efektů a mutable dat) úplně každý jazyk, protože konec konců, ty ÚPLNĚ VŽDY jenom říkáš interpretru (procesor) jaký side efekt má udělat a co má dělat po něm …

Přestaň prosím šířit bludy, už jich kolem FP pobíhá dost samo o sobě (tímto zdravím immutable cyklické struktury … :( )

Taco

Když koukám na kus kódu:

foo :: String -> Int -> String

Tak pokud mi vrací pro stejné dva argumenty stále stejný výsledek, tak je referenčně transparentní, a nemá side-efekty. A nezajímá mě jak toho ve skutečnosti dosahuje. (Leda, kdybych to chtěl dokazovat.) Zásadní je, že navenek se chová FP. Stejně tak není podstatné, že já se k němu budu chovat neFP, a budu třeba výsledek té funkce posílat do sítě.

A naopak.

To posuzování je vždy z pohledu volajícího.

Proto třeba imperativní monády mohou běhat ve funkcionálním kódu.

Proto nemáš pravdu.

Jiří Knesl

To je demagogie jako hrom.

Ne, dílo od Shakespeara není funkcionální kód.

Navíc i v samotném programu může být pure-FP program skutečně pure.

Když budu psát funkci, která přečte soubor, vezme první řádku, z té udělá uppercase a to vypíše na obrazovku.

Tak imperativně ten interpret DOOPRAVDY otevře soubor, přečte první řádku, udělá uppercase, to vypíše.

Zatímco v FP ten interpret DOOPRAVDY vrátí funkci „otevírající soubor“ s callbackem s daty, což se předá do funkce vracející první řádku, což padne do funkce uppercase, což padne do funkce, která vrátí callback „poté, co došlo k printu ven“.

Ten kód je čistě funkcionální i na runtime úrovni. Pak je tu úroveň pod tím, která to skutečně vyhodnocuje. Ta je ale neviditelná. V programu vývojář fakt píše funkce, vrací funkce a ono se to děje „vespod“, ale jinak. Vývojář nevolá funkci „načti data“, on vrátí funkci „poté, co jsi načetl data“ a v tom je velký rozdíl.

Co se šíření bludů týká, čumile, docela jste se vyznamenal.

tacoberu

Hmm, reaguješ na mě, nebo na čumila?

Radek Miček

podle týdle jebnutý logiky je funkcionální (bez side efektů a mutable dat) úplně každý jazyk

Tahle logika neříká nic o tom, zda je každý jazyk funkcionální, ta pouze říká to, že ve většině jazyků můžete mít drtivou většinu kódu bez vedlejších efektů a drtivá většina funkcí může být referenčně transparentní. Taková transformace jde udělat mechanicky. Například kdykoliv má v původní funkci proběhnout vedlejší efekt, např. readLine, tak funkci jednoduše ukončím a vrátím dvojici (ReadLine, Next), kde první prvek dvojice říká, co se má dělat za efekt, druhý prvek říká, co se má dělat poté. Funkce vracející dvojici je bez vedlejších efektů, a opakováním této transformace získáme i Next bez vedlejších efektů.

Tj. všechny funkce v původním programu mohu takto zbavit vedlejších efektů. Abych program mohl spouštět, vytvořím jednu funkci s vedlejšími efekty, ta bude v cyklu volat transformované funkce bez vedlejších efektů a vykonávat vedlejší efekty. Skončí se například v okamžiku, kdy je vrácen efekt Stop (první prvek dvojice je Stop).

ÚPLNĚ VŽDY jenom říkáš interpretru (procesor) jaký side efekt

Cílem je, aby fungoval substituční model v programu (funkce v programu byly referenčně transparentní) – nezáleží na tom, co se děje v procesoru/paměti počítače.

tacoberu

2) BTW tahle definice příliš dobře nefunguje v jazyce s vedlejšími efekty. Například mohu mít klasicky definovaný map, ale když mu jako funkci, pomocí níž se mapuje, dám rand, tak podle té vaší definice nebude referenčně transparentní (i když normálně bychom ho za referenčně transparentní považovali).

No, já bych ho v takovém případě za referenčně transparentní nepovažoval. Pokud umožní, aby jej funkce s vedlejšími efekty nakazila…

Honza

Kód, který autor uvádí jako „objektový“, je designově principálně špatně!

order = new Order(...)
invoice = order.generateInvoice(user)

Tímto se vnáší nežádoucí dependency – typ Order musí znát typ Invoice, což by neměl.

Místo toho by mělo být buď

invoice = new Invoice(order, user)

nebo ještě lépe

invoice = invoiceFactory(order, user)

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.