Uvod u funkcionalno programiranje

Marko Pavlović nas ‘od nule’ uvodi u funkcionalno programiranje.

Marko Pavlović
07/07/2016

Bazirajući se na matematičkim osnovama lambda kalkulusa, funkcionalno programiranje uvodi nekoliko fundamentalnih principa:

Funkcije višeg reda

Nepromenljivi podaci

Čiste funkcije

U nastavku ćemo razjasniti svaki od ovih pojmova ponaosob.

Funkcije višeg reda

U funkcionalnim jezicima funkcija je osnovni gradivni blok vašeg koda. Kao što u objektno orijentisanim jezicima možemo da kreiramo i koristimo objekte, tako na isti način u funkcionalnim jezicima možemo da kreiramo i koristimo funkcije.

Jednostavno rečeno – možemo kreirati funkciju, dodeliti je nekoj promenljivoj ili je proslediti drugoj funkciji kao argument.

Funkcija višeg reda je ona funkcija koja može da prihvati neku drugu funkciju kao svoj argument.

Ovo je veoma moćan koncept koji omogućava jednostavnu i prirodnu implementaciju inverzije kontrole, koji je jedan od najvažnijih principa u razvoju softvera.

Možemo reći da obrasci u objektno-orijentisanom programiranju kao što su:

Template method

Strategy

Visitor

predstavljaju samo pokušaje da se nadomesti podrška za funkcije višeg reda u tim jezicima.

U funkcionalnom programiranju se sve zasniva na ideji kompozicije jednostavnih funkcija, kako bi se dobila kompleksna struktura programa. Odličan primer ovoga možemo videti u programskom jeziku Elixir i njegovoj dokumentaciji za Enum modul.

Nepromenljivi podaci

Ukratko, kada želimo da promenimo stanje nekog objekta, mi najpre napravimo kopiju originalnog objekta, a zatim izmenimo kopiju umesto originala.

Zašto? Na ovaj način je mnogo jednostavnije razmišljati o kodu.

Kada prosledimo neki podatak funkciji kao argument, mi očekujemo da će pri izvršenju te funkcije podatak imati istu vrednost kao kada je funkcija pozvana. Na primer, neka je data funkcija koja prima 2 parametra – a i b i računa njihov zbir. Kada toj funkciji prosledimo 1 i 2, očekujemo da na izlazu dobijemo 3 – očigledno, zar ne?

U generalnom slučaju to ne mora da bude tako…

U većini programskih jezika, kada prosledimo objekat kao parametar neke funkcije, mi zapravo prosleđujemo samo referencu na dati objekat, a ne njegovu vrednost. Ovakav način prenošenja parametara ne garantuje inicijalnu, gotovo trivijalnu pretpostavku koju smo gore izneli.

Na primer

Neka naš program ima dve programske niti, i neka obe niti čuvaju referencu na jedan objekat u memoriji. Recimo da taj objekat predstavlja osobu koja se zove “Petar Perić”. Razmotrimo sledeću sekvencu događaja:

Nit A poziva funkciju query_database i prosleđuje joj kao parametar objekat koji predstavlja osobu “Petar Perić”.

Na polovini izvršavanja funkcije query_database, nit A se pauzira od strane operativnog sistema i sa izvršavanjem kreće nit B.

Nit B promeni ime na zajedničkom objektu “Petar Perić” u “Nikola Perić”.

Izvršavanje prve niti se nastavlja od sredine funkcije query_database, gde je i bila prekinuta, ali bez ikakvog znanja da se ime osobe promenilo sa “Petar” na “Nikola”.

Sada je očigledno da će izvršeni upit na kraju funkcije query_database biti različit od očekivanog.

Ovakav tip problema u programskom kodu može da uzrokuje veliku glavobolju jer:

Ne dolazi do nastanka nikakve greške i program se izvršava naizgled bez ikakvih problema, tako da najverovatnije prolazi unit testove.

Problem se javlja samo povremeno, jer je za njegovo nastajanje potrebno da se funkcija query_database prekine na tačno određenom mestu, tako da program verovatno prolazi i ručno testiranje ili QA.

Program radi savršeno za većinu pravih korisnika aplikacije.

Međutim, s vremena na vreme desi se greška koja utične na rad samo nekolicine naizgled nasumičnih korisika koji nemaju ništa zajedničko, pa je veoma teško ovakvu grešku reprodukovati u lokalnom razvojnom okruženju.

Bez sumnje ovakav problem može vrlo lako da izazove da poneki frustrirani korisnici napuste vašu aplikaciju.

Da bismo izbegli ovakav problem, pretpostavka da podaci sa kojima radimo neće biti promenjeni od strane nekog drugog dela koda, mora da bude tačna.

Kao što se vrlo često pokazuje u praksi, najjednostavnija rešenja su najčešće i najbolja. Tako da je pravljenje potpuno nove kopije podatka pre njegove izmene ono čemu su konvergirali mnogi funkcionalni jezici danas.

Čiste funkcije

Funkcija predstavlja preslikavanje ili relaciju između njenih ulaza i izlaza. Bez obzira koliko je jednostavno ili komplikovano to preslikavanje moramo voditi računa o tome da naš sistem uvek radi pouzdano.

I ako je ograničenje koje uvode nepromenljivi podaci korak u pravom smeru, mi kao autori funkcija se ipak moramo pridržavati “pravila lepog ponašanja” u funkcionalnom programiranju. Postoje dva osnovna pravila kod pisanja “čistih funkcija”:

Ako “čistu funkciju” pozovemo više puta za iste ulazne podatke, dobićemo isti izlaz svaki put.

Dok izračunava izlaz, čista funkcija neće sprečiti nijednu drugu funkciju da poštuje pravilo broj 1.

U mnogim programskim jezicima, ova dva pravila se mogu sažeti u samo jedno pravilo – čista funkcija nikada ne menja globalno stanje programa.

Ovo znači da mi samo uočavamo relacije i veze između postojećih objekata i onda pišemo funkcije koje te relacije izražavaju u kodu.

Prednosti funkcionalnog programiranja

Činjenica da funkcija konzistentno vraća isti rezultat za iste ulazne vrednosti, dozvoljava nam da testiramo datu funkciju kao crnu kutiju. Takođe je vrlo lako primeniti tehnike keširanja rezultata, kao što je Memoizacija.

Paralelizacija koda postaje jednostavna, jer kada znamo da je dovoljno dostaviti samo ulazne podatke funkcije da bismo je pozvali, tada tu funkciju možemo izvršavati i na drugom računaru ili skupu računara, sve dok možemo da pošaljemo njene ulazne parametre.

Višenitne aplikacije napisane pomoću funkcionalnog programiranja nikada ne moraju da direktno rešavaju problem proizvođač potrošač. Umesto toga sva interakcija između programskih niti obavlja se putem razmene neblokirajućih poruka. Programski jezik Elixir ovo koristi na spektakularan način.

Naš kod postaje modularan i jednostavan za razumevanje, što poboljšava produktivnost razvojnog tima i pouzdanost krajnjeg proizvoda.

Bacite pogled i na drugi deo.

Marko Pavlović

Objavio/la članak.

četvrtak, 7. Jul, 2016.

IT Industrija

🔥 Najčitanije

Marko Pavlović

subota, 16. Jul, 2016.

Hvala Novače na dodatnom materijalu za razmišljanje i na dobrom komentaru :) Jako su mi zanimljivi pristupi u ovim bibliotekama za Closure, i ako nisam stručnjak za taj jezik :)

Novak

četvrtak, 14. Jul, 2016.

Bravo za odabir teme za diskusiju! :) Dodao bih još neke od problema: - funkcionalno modelovanje zahteva potpuno nov pristup problemu od onog u OOP-u, - pristup enkapsulaciji i polimorfizmu je malko drugačiji od onog u OOP (u jeziku sa kojim imam iskustva tu uskaču koncepti ad-hoc hierarhija, multimetoda, protokola) - Lako je upasti u zamku i koristiti lenje koncepte tamo gde se ne želi I još mnogo toga. Ali pored svih problema FP donosi i mnoga rešenja koja su veoma dopadljiva. * Evo i nekih Clojure rešenja za problem koji navodi Marko: https://github.com/stuartsierra/component https://github.com/tolitius/mount

Marko Pavlović

četvrtak, 7. Jul, 2016.

Zdravo Ivane, to je jako dobro pitanje! Naravno da nije sve sjajno i bajno, a najveći broj problema nastaje upravo zbog činjenice da je za interakciju sa spoljašnjim svetom neophodno održavati globalno stanje aplikacije, što se kosi sa premisama čistih funkcija. Na primer u "realnim" aplikacijama je često potrebno čuvati informacije kao što su: trenutno ulogovan korisnik, stanje transakcije sa bazom podataka ili nekim drugim servisom, itd... Tada se postavlja pitanje koji deo aplikacije će biti odgovoran za to. Najbolje rešenje je da se odredi jedna pristupna tačka preko koje se odvija sva razmena podataka sa spoljašnjim svetom. Naravno postoje i drugi "trikovi", gde se na primer celokupno stanje prenosi kroz parametre jedne funkcije, i sl. Takav pristup koristi Eliksir jezik u svojim GenServer-ima.

Иван

četvrtak, 7. Jul, 2016.

Шта су мане функционалног програмирања? Немогуће да их нема, да је све сјајно и бајно.