Bevezetés a funkcionális programozás alapelveihez

Hosszú idő után megtanultam és dolgoztam az objektum-orientált programozással, egy lépéssel hátrébb mentem, hogy átgondoljam a rendszer komplexitását.

" Complexity is anything that makes software hard to understand or to modify." - John Outerhout

Néhány kutatás után olyan funkcionális programozási fogalmakat találtam, mint a változtathatatlanság és a tiszta funkció. Ezek a fogalmak nagy előnyöket jelentenek a mellékhatásoktól mentes funkciók kiépítésében, így könnyebb fenntartani a rendszereket - néhány más előnnyel együtt.

Ebben a bejegyzésben többet fogok mondani a funkcionális programozásról és néhány fontos fogalomról, sok kód példával.

Ez a cikk a Clojure programozási nyelv példaként használja a funkcionális programozás magyarázatát. Ha nem kedveli a LISP-típusú nyelveket, ugyanazt a bejegyzést JavaScriptben is közzétettem. Vessen egy pillantást: Funkcionális programozási alapelvek Javascriptben

Mi a funkcionális programozás?

A funkcionális programozás olyan programozási paradigma - a számítógépes programok struktúrájának és elemeinek felépítésének stílusa -, amely a számítást matematikai függvények kiértékelésének tekinti, és elkerüli a változó állapotú és a mutábilis adatokat - Wikipedia

Tiszta funkciók

Az első alapvető fogalom, amelyet akkor tanulunk meg, amikor meg akarjuk érteni a funkcionális programozást, a tiszta függvények . De mit is jelent ez valójában? Mitől tiszta a függvény?

Tehát honnan tudjuk, hogy egy függvény-e purevagy sem? Itt van a tisztaság nagyon szigorú meghatározása:

  • Ugyanazokat az eredmény, ha az adott ugyanazokat az érveket (ez is nevezik deterministic)
  • Nem okoz megfigyelhető mellékhatásokat

Ugyanazt az eredményt adja vissza, ha ugyanazokat az érveket adják meg neki

Képzelje el, hogy egy olyan függvényt szeretnénk megvalósítani, amely kiszámítja egy kör területét. Egy tisztátalan függvény kap radiusparaméterként, majd kiszámítja radius * radius * PI. A Clojure-ban az operátor az első, így radius * radius * PIválik (* radius radius PI):

Miért ez a tisztátalan funkció? Egyszerűen azért, mert olyan globális objektumot használ, amelyet nem adtak át a függvény paramétereként.

Most képzelje el, hogy egyes matematikusok azzal érvelnek, hogy az PIérték valóban az, 42és megváltoztatják a globális objektum értékét.

A tisztátalan függvényünk most 10 * 10 * 42= 4200. Ugyanarra a paraméterre ( radius = 10) más eredményt kapunk. Javítsuk ki!

TA-DA? Most mindig átadjuk a P I értéket paraméterként a függvénynek. Tehát most csak a függvényhez továbbított paraméterekhez férünk hozzá. Nincs external object.

  • A radius = 10& paramétereknél PI = 3.14mindig ugyanaz az eredmény lesz:314.0
  • A radius = 10& paramétereknél PI = 42mindig ugyanaz az eredmény lesz:4200

Fájlok olvasása

Ha a függvényünk külső fájlokat olvas, akkor ez nem tiszta funkció - a fájl tartalma megváltozhat.

Véletlen számgenerálás

Bármely függvény, amely véletlenszám-generátorra támaszkodik, nem lehet tiszta.

Nem okoz megfigyelhető mellékhatásokat

A megfigyelhető mellékhatások közé tartozik egy globális objektum vagy egy referenciaként átadott paraméter módosítása.

Most egy függvényt szeretnénk megvalósítani, hogy egy egész értéket kapjon, és az 1-gyel növelt értéket adja vissza.

Megvan az counterértéke. A tisztátalan függvényünk megkapja ezt az értéket, és újra hozzárendeli a számlálót az 1-gyel növelt értékkel.

Megfigyelés : a funkcionális programozásban a mutabilitás nem javasolt.

Módosítjuk a globális objektumot. De hogyan csinálnánk pure? Csak adja vissza az 1-gyel növelt értéket. Egyszerű.

Lásd, hogy a tiszta függvényünk increase-counter2-t ad vissza, de az counterértéke továbbra is ugyanaz. A függvény a növelt értéket adja vissza a változó értékének megváltoztatása nélkül.

Ha betartjuk ezt a két egyszerű szabályt, akkor könnyebben megértjük programjainkat. Most minden funkció elszigetelt és nem képes befolyásolni a rendszer más részeit.

A tiszta funkciók stabilak, következetesek és kiszámíthatók. Ugyanazon paraméterek mellett a tiszta függvények mindig ugyanazt az eredményt adják. Nem kell olyan helyzetekre gondolnunk, amikor ugyanazon paraméter különböző eredményekkel jár - mert ez soha nem fog megtörténni.

A tiszta funkciók előnyei

A kódot mindenképpen könnyebb tesztelni. Semmit sem kell gúnyolnunk. Tehát a tiszta funkciókat különböző kontextusokkal tesztelhetjük:

  • Adott paraméter A→ várjuk, hogy a függvény visszaadja az értéketB
  • Adott paraméter C→ várjuk, hogy a függvény visszaadja az értéketD

Egyszerű példa egy olyan funkció, amely megkapja a számok gyűjteményét, és elvárja, hogy növelje a gyűjtemény minden elemét.

Megkaptuk a numbersgyűjteményt, mapa incfüggvénnyel használjuk az egyes számok növelését, és a növekményes számok új listájának visszaküldését.

A input[1 2 3 4 5]várhatóan az outputlenne [2 3 4 5 6].

Állandóság

Idővel nem változik, vagy nem lehet megváltoztatni.

Ha az adatok megváltoztathatatlanok, azok állapota nem változhatlétrehozása után. Ha módosíthatatlan objektumot szeretne megváltoztatni, akkor nem. Ehelyett létrehoz egy új objektumot az új értékkel.

A Javascriptben általában a forciklust használjuk . Ez a következő forállítás tartalmaz néhány változtatható változót.

Minden iterációnál megváltoztatjuk a iés az sumOfValueállapotot . De hogyan kezeljük a mutációt az iterációban? Rekurzió! Vissza a Clojure-hoz!

Tehát itt van az a sumfüggvény, amely numerikus értékek vektorát fogadja. A recurugrik vissza a loopamíg megkapjuk a vektor üres (a rekurzió base case). Minden „iterációhoz” hozzáadjuk az értéket az totalakkumulátorhoz.

Rekurzióval megtartjuk a változókatváltozhatatlan.

Megfigyelés : Igen! Használhatjuk reduceezt a funkciót. Ezt látni fogjuk a Higher Order Functionstémában.

Nagyon gyakori az objektum végső állapotának felépítése is . Képzelje el, hogy van egy karakterláncunk, és ezt a karakterláncot a-vá akarjuk alakítani url slug.

A rubin OOP-ban létrehoznánk egy osztályt, mondjuk UrlSlugify. Ennek az osztálynak lesz egy slugify!módja a karakterlánc bemenetének a-vá alakítására url slug.

Gyönyörű! Megvalósult! Itt meg kell adnunk a programozást, amely pontosan megmondja, hogy mit akarunk tenni az egyes slugifyfolyamatokban - először kisbetűvel írjuk be, majd távolítsuk el a haszontalan szóközöket, végül pedig a fennmaradó fehér helyeket kötőjellel helyettesítsük.

De ebben a folyamatban a bemeneti állapotot mutáljuk.

Ezt a mutációt kezelhetjük függvényösszetétel vagy függvényláncolással. Más szavakkal, egy függvény eredményét felhasználjuk bemenetként a következő függvényhez, az eredeti bemeneti karakterlánc módosítása nélkül.

Itt van:

  • trim: eltávolítja a szóközt a karakterlánc mindkét végén
  • lower-case: a karakterláncot kisbetűvé alakítja
  • replace: az egyezés összes példányát helyettesíti egy adott karakterláncban

Kombináljuk mindhárom funkciót, és meg tudjuk "slugify"csinálni a karakterláncunkat.

Ha a funkciók kombinálásáról beszélünk, akkor a compfunkcióval mindhárom funkciót összeállíthatjuk. Lássuk:

Referenciális átláthatóság

Vezessünk be egy square function:

Ennek a (tiszta) függvénynek mindig ugyanaz a kimenete lesz, ugyanazt a bemenetet kapva.

A „2” átadása a square functionvégrendelet paramétereként mindig a 4. értéket adja vissza. Tehát most (square 2)a 4-et helyettesíthetjük . Ennyi! A mi feladatunk az referentially transparent.

Alapvetően, ha egy függvény következetesen ugyanazt az eredményt adja ugyanarra a bemenetre, akkor referenciálisan átlátszó.

tiszta funkciók + megváltoztathatatlan adatok = referenciális átláthatóság

Ezzel a koncepcióval remek dolog, amit tehetünk, ha megjegyezzük a funkciót. Képzelje el, hogy rendelkezünk ezzel a funkcióval:

Az (+ 5 8)egyenlő 13. Ez a funkció mindig azt eredményezi 13. Tehát ezt megtehetjük:

És ez a kifejezés mindig azt eredményezi 16. Helyettesíthetjük a teljes kifejezést numerikus állandóval, és megjegyezhetjük.

Első osztályú entitásként funkcionál

A funkciók első osztályú entitásként való elgondolása az, hogy a függvényeket szintén értékként kezeljük, és adatokként használjuk.

A Clojure-ban gyakran használják defna függvények meghatározására, de ez csak a szintaktikai cukor (def foo (fn ...)). fnmagát a függvényt adja vissza. defna-t ad vissza, varamely egy függvényobjektumra mutat.

Az első osztályú entitásokként funkcionálhatnak:

  • állandókból és változókból hivatkozhat rá
  • adja át paraméterként más funkcióknak
  • adja vissza más funkciók eredményeként

Az elképzelés az, hogy a függvényeket értékként kezeljük, és olyan funkciókat adunk át, mint az adatok. Így kombinálhatjuk a különböző funkciókat, hogy új funkciókat hozzunk létre új viselkedéssel.

Képzelje el, hogy van egy olyan függvényünk, amely két értéket összegez, majd megduplázza az értéket. Valami ilyesmi:

Most egy olyan függvény, amely kivonja az értékeket és a kettőt adja vissza:

Ezeknek a függvényeknek hasonló logikájuk van, de a különbség az operátorok függvénye. Ha a függvényeket értékként kezelhetjük, és ezeket argumentumként adhatjuk meg, akkor létrehozhatunk egy olyan függvényt, amely megkapja az operátor függvényt, és felhasználhatja azt a függvényünkön belül. Építsük meg!

Kész! Most van egy férvünk, és felhasználjuk a aés b. Átadtuk a +és a -függvényeket, hogy összeírjuk a double-operatorfüggvénnyel és új viselkedést hozzunk létre.

Magasabb rendű funkciók

Ha magasabb rendű függvényekről beszélünk, akkor olyan funkciókat értünk, amelyek:

  • - egy vagy több függvényt vesz fel argumentumként, vagy
  • függvényt ad eredményül

A double-operatorfent megvalósított függvény egy magasabb rendű függvény, mert argumentumként operátorfüggvényt vesz fel és használja.

Ön valószínűleg már hallott filter, mapés reduce. Vessünk egy pillantást ezekre.

Szűrő

Adott gyűjteményt szeretnénk egy attribútum alapján szűrni. A szűrő függvény arra számít, hogy a truevagy az falseérték meghatározza, hogy az elemet be kell- e vonni az eredménygyűjteménybe. Alapvetően, ha a visszahívási kifejezés az true, akkor a szűrő funkció tartalmazza az elemet az eredménygyűjteményben. Ellenkező esetben nem fog.

Egyszerű példa, amikor egész számok gyűjteménye van, és csak a páros számokat akarjuk.

Kötelező megközelítés

A Javascript használatának elengedhetetlen módja:

  • hozzon létre egy üres vektort evenNumbers
  • iterál a numbersvektor felett
  • tolja a páros számokat a evenNumbersvektorhoz

Használhatjuk a filtermagasabb rendű függvényt a even?függvény fogadására , és visszaadhatunk páros számok listáját:

Egy érdekes probléma, amelyet a Hacker Rank FP Path-on megoldottam, a Filter Array probléma volt . A probléma ötlete az, hogy kiszűr egy adott egész tömböt, és csak azokat az értékeket adja ki, amelyek kisebbek, mint egy megadott érték X.

A probléma elengedhetetlen Javascript-megoldása:

Pontosan elmondjuk, hogy mit kell tennünk a funkciónknak - ismételjük meg a gyűjteményt, hasonlítsuk össze a gyűjtemény aktuális elemét x, és toljuk ezt az elemet resultArraya feltétel teljesítéséhez.

Deklaratív megközelítés

De szeretnénk egy deklaratívabb módszert a probléma megoldására, és a filtermagasabb rendű függvény használatát is.

A deklaratív Clojure megoldás valami ilyesmi lehet:

Ez a szintaxis eleve kissé furcsának tűnik, de könnyen érthető.

#(> xA%) csak egy névtelen függvény, amely megkapja es x-t és összehasonlítja a collectio minden elemével n. A% a névtelen függvény paraméterét jelenti - ebben az esetben a t he filter belsejében lévő aktuális elemet .

Térképekkel is megtehetjük. Képzelje el, hogy van egy térképünk az emberekről a nameés age. És csak egy meghatározott életkor felettieket akarunk szűrni, ebben a példában a 21 évnél idősebbeket.

A kód összefoglalása:

  • van egy listánk emberekről (és nameés age).
  • megvan a névtelen függvény #(< 21 (:age %)). Ne feledje, hogy t he% a gyűjtemény jelenlegi elemét képviseli? Nos, a gyűjtemény eleme egy néptérkép. Ha do (:age {:name "TK" :age 26}), akkor e,ebben az esetben a 26 éves korértéket adja vissza .
  • minden embert szűrünk e névtelen funkció alapján.

Térkép

A térkép ötlete egy gyűjtemény átalakítása.

A mapmódszer úgy alakít át egy gyűjteményt, hogy egy függvényt alkalmaz az összes elemére, és a visszaadott értékekből egy új gyűjteményt épít.

Nézzük meg ugyanazt a peoplegyűjteményt fent. Most nem akarunk szűrni „kor felett”. Csak egy húrok listáját akarjuk, ilyesmi TK is 26 years old. Tehát az utolsó karakterlánc lehet a gyűjtemény egyes elemeinek :name is :age years oldhol :nameés :ageattribútuma people.

Javascript nélkülözhetetlen módon ez a következő lenne:

Deklaratív Clojure módon ez lenne:

Az egész elképzelés az, hogy egy adott gyűjteményt új kollekcióvá alakítsunk át.

Egy másik érdekes Hacker Rank probléma a frissítési lista probléma volt . Csak egy adott gyűjtemény értékeit szeretnénk frissíteni abszolút értékeikkel.

Például a bemenetnek [1 2 3 -4 5]a kimenetnek kell lennie [1 2 3 4 5]. Az abszolút értéke -4van 4.

Egyszerű megoldás az egyes gyűjteményértékek helyben történő frissítése lenne.

A Math.absfüggvény segítségével átalakítjuk az értéket abszolút értékévé, és elvégezzük a helyben történő frissítést.

Ez a megoldás megvalósításának nem funkcionális módja.

Először megismertük a változhatatlanságot. Tudjuk, mennyire fontos a változtathatatlanság, hogy következetesebbé és kiszámíthatóbbá tegyük funkcióinkat. Az ötlet egy új, minden abszolút értéket tartalmazó gyűjtemény felépítése.

Másodszor, miért nem használhatja mapitt az összes adat "átalakítását"?

Az első ötletem az volt, hogy olyan to-absolutefunkciót építsek fel, amely csak egy értéket kezel.

Ha negatív, akkor pozitív értékben (abszolút értékben) akarjuk átalakítani. Ellenkező esetben nem kell átalakítanunk.

Most, hogy tudjuk, hogyan kell elvégezni absoluteegy értéket, használhatjuk ezt a függvényt arra, hogy argumentumként átadjuk a mapfüggvénynek. Emlékszel arra, hogy a higher order functiona megkapja a függvényt argumentumként és használhatja? Igen, a térkép képes rá!

Azta. Olyan gyönyörű! ?

Csökkentse

A redukció ötlete egy függvény és egy gyűjtemény megkapása, valamint az elemek kombinálásával létrehozott érték visszaadása.

Az emberek általánosan elterjedt példa a megrendelés teljes összegének megszerzése. Képzelje el, hogy egy bevásárló webhelyen volt. Ön megadta Product 1, Product 2, Product 3és Product 4a kosárba (sorrendben). Most a kosár teljes összegét szeretnénk kiszámítani.

Feltétlenül megismételjük a rendelési listát, és az egyes termékeket összeadjuk a teljes összeggel.

Használatával reducefelépíthetünk egy függvényt a (z) kezelésére, amount sumés argumentumként átadhatjuk a reducefüggvénynek.

Itt van az shopping-carta funkció, sum-amountamely fogadja az áramot total-amount, és az current-productobjektum sumnekik.

A get-total-amountfunkció használható reducea shopping-cartsegítségével a sum-amountkiindulva 0.

A teljes összeg megszerzésének másik módja a komponálás mapés reduce. Mit akarok ezzel mondani? Használhatjuk maparra, hogy az értéket értékek shopping-cartgyűjteményévé alakítsuk át amount, majd csak a reducefüggvényt használjuk a +funkcióval.

A get-amountfogadja a termék objektumot, és csak az amountértéket adja vissza . Tehát itt van az [10 30 20 60]. Ezután az reduceösszes elemet összeadja az összesítéssel. Gyönyörű!

Megnéztük, hogyan működnek az egyes magasabb rendű függvények. Szeretnék mutatni egy példát arra, hogyan állíthatjuk össze mindhárom függvényt egy egyszerű példában.

Beszél shopping cart, képzeljük mi ezt a listát a termékek a sorrendben:

Azt akarjuk, hogy a bevásárlókosárban található összes könyv összege legyen. Egyszerű a dolog. Az algoritmus?

  • szűrés könyvtípus szerint
  • transzformálja a bevásárlókosarat a térkép segítségével összegösszeggé
  • kombinálja az összes elemet összeadva redukcióval

Kész! ?

Erőforrások

Rendeztem néhány forrást, amit olvastam és tanulmányoztam. Megosztom azokat, amelyeket igazán érdekesnek találtam. További forrásokért keresse fel a Funkcionális programozás Github tárhelyemet .

  • Rubin specifikus források
  • Javascript-specifikus források
  • Clojure-specifikus források

Bevezetések

  • FP tanulása JS-ben
  • Intro do FP Python-nal
  • Az FP áttekintése
  • Gyors bevezető a funkcionális JS-hez
  • Mi az FP?
  • Funkcionális programozási szakszó

Tiszta funkciók

  • Mi a tiszta funkció?
  • Tiszta funkcionális programozás 1
  • Tiszta funkcionális programozás 2

Változhatatlan adatok

  • Változhatatlan DS a funkcionális programozáshoz
  • Miért a közös megváltoztatható állapot minden gonosz gyökere
  • Strukturális megosztás a Clojure-ban: 1. rész
  • Strukturális megosztás a Clojure-ban: 2. rész
  • Strukturális megosztás a Clojure-ban: 3. rész
  • Strukturális megosztás a Clojure-ban: utolsó rész

Magasabb rendű funkciók

  • Beszédes JS: magasabb rendű funkciók
  • Szórakoztató szórakoztató funkció szűrő
  • Fun fun fun Map
  • Fun fun fun function Basic Reduce
  • Fun fun function Advanced Reduce
  • Clojure magasabb rendű funkciók
  • Tisztán funkciós szűrő
  • Tisztán funkcionális térkép
  • Tisztán funkcionális csökkentés

Nyilatkozatos programozás

  • Deklaratív programozás vs kötelező

Ez az!

Hé emberek, remélem, jól éreztétek magatokat ezzel a bejegyzéssel, és remélem, hogy itt sokat tanultatok! Ez volt a kísérletem arra, hogy megosszam, amit tanulok.

Itt található a lerakat a cikk összes kódjával .

Gyere velem tanulni. Az erőforrásokat és a kódomat megosztom ebben a Learning Functional Programming adattárban .

Remélem, láttál itt valami hasznosat számodra. És legközelebb találkozunk! :)

Saját Twitter és Github. ☺

TK.