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

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 nodnotu". 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. Ikdyž 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.

Programování

Předejte zkušenosti i dalším a sdílejte tento článek!



Jiří Knesl
Business & IT konzultant

Jiří Knesl poprvé začal programovat v roce 1993. Od té doby, díky skvělým učitelům a později zákazníkům, měl možnost neustále růst v oboru vývoje webových aplikací a informačních systémů. v roce 2002 se přidal zájem o ekonomii a v roce 2006 o organizaci práce. Vším tím se konstantně profesně zabývá jak ve svém podnikání, tak i u zákazníků. Za posledních 5 let vydal na tato témata přes 400 článků.

Prohlédněte si moje reference

Mám zkušenosti z rozsáhlých projektů pro korporace, velké podniky, střední i malé firmy, ale i pro startupy v cloudu. Zvyšoval jsem jejich know-how, pomáhal nastavovat jejich organizační strukturu, byl lektorem a mentorem v náročných situacích. Podívejte se, jak vidí můj přínos samotní klienti.

Sledujte mé postřehy na sociálních sítích