Jiří Knesl: 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.

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).

Jiří Knesl se zabývá hlavně Scrumem a správným vývojem software (prevence chyb, vyšší produktivita).

Komentáře: 56

Přehled komentářů

tacoberu pure fp a pure oop
Jiří Knesl Re: pure fp a pure oop
Atamiri Re: pure fp a pure oop
v6ak Scala a domunantni pristup
Jiří Knesl Re: Scala a domunantni pristup
v6ak Re: Scala a domunantni pristup
v6ak Ciste objektovy pristup
Jiří Knesl Re: Ciste objektovy pristup
x86 Re: Ciste objektovy pristup
v6ak Ciste funkcionalni pristup...
v6ak Re: Ciste funkcionalni pristup...
v6ak Re: Ciste funkcionalni pristup...
Ondřej Novák hrušky jabka
tacoberu Re: hrušky jabka
Ondřej Novák Re: hrušky jabka
tacoberu Re: hrušky jabka
Ondřej Novák Re: hrušky jabka
Taco Re: hrušky jabka
falken Re: hrušky jabka
Jiří Knesl Re: hrušky jabka
balki Re: hrušky jabka
balki Re: hrušky jabka
Jiří Knesl Re: hrušky jabka
balki Re: hrušky jabka
Jiří Knesl Re: hrušky jabka
Ondřej Novák Re: hrušky jabka
Jiří Knesl Re: hrušky jabka
Ondřej Novák Re: hrušky jabka
Jiří Knesl Re: hrušky jabka
Ondřej Novák Re: hrušky jabka
tacoberu Re: hrušky jabka
Jirka Kosek Re: hrušky jabka
Jiří Knesl Re: hrušky jabka
jirkakosek Re: hrušky jabka
v6ak Re: hrušky jabka
tacoberu Re: hrušky jabka
Palo Re: hrušky jabka
tacoberu Re: hrušky jabka
Ondřej Novák hrušky jabka
balki
Oldis
Ondřej Novák Re:
Oldis Re:
Pavel Strnad Monady a jine zajimavosti
Jiří Knesl Re: Monady a jine zajimavosti
David Fogaš Není program jako program
Jiří Knesl Re: Není program jako program
Radek Miček
Radek Miček Re:
čumil Re:
Taco Re:
Jiří Knesl Re:
tacoberu Re:
Radek Miček Re:
tacoberu Re:
Honza Špatný příklad v článku
Zdroj: https://www.zdrojak.cz/?p=18249