Sissejuhatus funktsionaalse programmeerimise aluspõhimõtetesse

Pärast pikka aega objektorienteeritud programmeerimisega õppimist ja töötamist astusin sammu tagasi, et süsteemi keerukusele mõelda.

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

Mõningaid uuringuid tehes leidsin funktsionaalsed programmeerimiskontseptsioonid nagu muutumatus ja puhas funktsioon. Need kontseptsioonid on kõrvaltoimeteta funktsioonide loomisel suured eelised, seega on süsteemide hooldamine lihtsam - koos mõnede muude eelistega.

Selles postituses räägin teile rohkem funktsionaalsest programmeerimisest ja mõnedest olulistest mõistetest koos paljude koodinäidetega.

Selles artiklis kasutatakse funktsionaalse programmeerimise selgitamiseks Clojure'i kui programmeerimiskeele näidet. Kui te ei tunne LISP-tüüpi keelt, avaldasin sama postituse ka JavaScripti. Heitke pilk: funktsionaalsed programmeerimispõhimõtted Javascriptis

Mis on funktsionaalne programmeerimine?

Funktsionaalne programmeerimine on programmeerimisparadigma - arvutiprogrammide struktuuri ja elementide ülesehitamise stiil -, mis käsitleb arvutamist matemaatiliste funktsioonide hindamisena ja väldib muutuva oleku ja muutuvaid andmeid - Wikipedia

Puhtad funktsioonid

Esimene põhimõiste, mille õpime, kui tahame mõista funktsionaalset programmeerimist, on puhtad funktsioonid . Aga mida see tegelikult tähendab? Mis teeb funktsiooni puhtaks?

Niisiis, kuidas me saame teada, kas funktsioon on purevõi mitte? Siin on puhtuse väga range määratlus:

  • See tagastab sama tulemuse, kui talle antakse samad argumendid (sellele viidatakse ka kui deterministic)
  • See ei põhjusta jälgitavaid kõrvaltoimeid

See annab sama tulemuse, kui anda samad argumendid

Kujutage ette, et me tahame rakendada funktsiooni, mis arvutab ringi pinna. Ebapuhas funktsioon saadakse radiusparameetrina ja arvutatakse seejärel radius * radius * PI. Clojures on operaator esikohal, seega radius * radius * PIsaab temast (* radius radius PI):

Miks on see ebapuhas funktsioon? Lihtsalt sellepärast, et see kasutab globaalset objekti, mida funktsiooni parameetrina ei edastatud.

Kujutage nüüd ette, et mõned matemaatikud väidavad, et PIväärtus on tegelikult, 42ja muudavad globaalse objekti väärtust.

Meie ebapuhas funktsioon annab nüüd tulemuseks 10 * 10 * 42= 4200. Sama parameetri ( radius = 10) puhul on meil erinev tulemus. Parandame selle ära!

TA-DA? Nüüd edastame I väärtusele P alati parameetrina funktsiooni. Nii et nüüd pääseme juurde vaid funktsioonile edastatud parameetritele. Ei external object.

  • Parameetrite radius = 10& PI = 3.14puhul on meil alati sama tulemus:314.0
  • Parameetrite radius = 10& PI = 42puhul on meil alati sama tulemus:4200

Failide lugemine

Kui meie funktsioon loeb väliseid faile, pole see puhas funktsioon - faili sisu võib muutuda.

Juhuslike arvude genereerimine

Ükski juhuslike arvude generaatorile toetuv funktsioon ei saa olla puhas.

See ei põhjusta jälgitavaid kõrvaltoimeid

Vaadeldavate kõrvalnähtude näited hõlmavad globaalse objekti või viitena edastatud parameetri muutmist.

Nüüd tahame rakendada funktsiooni täisarvu saamiseks ja 1-ga suurendatud väärtuse tagastamiseks.

Meil on counterväärtus. Meie ebapuhas funktsioon võtab selle väärtuse vastu ja määrab loenduri uuesti väärtusega, mida on suurendatud 1 võrra.

Vaatlus : funktsionaalses programmeerimises ei soovitata mutatiivsust.

Muudame globaalset objekti. Aga kuidas me sellega hakkama saaksime pure? Lihtsalt tagastage väärtus, mida on suurendatud 1 võrra. Lihtne.

Vaadake, et meie puhas funktsioon increase-countertagastab 2, kuid counterväärtus on endiselt sama. Funktsioon tagastab suurenenud väärtuse muutuja väärtust muutmata.

Kui järgime neid kahte lihtsat reeglit, on meie programmidest lihtsam aru saada. Nüüd on iga funktsioon isoleeritud ega suuda mõjutada meie süsteemi teisi osi.

Puhtad funktsioonid on stabiilsed, järjepidevad ja prognoositavad. Võttes arvesse samu parameetreid, tagastavad puhtad funktsioonid alati sama tulemuse. Me ei pea mõtlema olukordadele, kus sama parameetri tulemused on erinevad - sest seda ei juhtu kunagi.

Puhaste funktsioonide eelised

Koodi on kindlasti lihtsam testida. Meil pole vaja midagi mõnitada. Nii saame testida puhtaid funktsioone erinevate kontekstidega:

  • Antud parameeter A→ oodake, et funktsioon tagastaks väärtuseB
  • Antud parameeter C→ oodake, et funktsioon tagastaks väärtuseD

Lihtne näide oleks funktsioon saada numbrite kogu ja eeldada, et see suurendab selle kogu iga elementi.

Saame numberskogu kätte, kasutame mapkoos incfunktsiooniga iga numbri suurendamiseks ja tagastame uue kasvavate arvude loendi.

Sest input[1 2 3 4 5]oodatud outputoleks [2 3 4 5 6].

Muutmatus

Aja jooksul muutumatu või seda ei saa muuta.

Kui andmed on muutumatud, ei saa nende olek muutudapärast selle loomist. Kui soovite muutumatut objekti muuta, ei saa te seda teha. Selle asemel loote uue objekti uue väärtusega.

Javascriptis kasutame tavaliselt fortsüklit. Selles järgmises foravalduses on mõned muutlikud muutujad.

Iga korduse puhul muudame olekuti ja sumOfValueolekut . Aga kuidas me korduvuse korral hakkama saame? Rekursioon! Tagasi Clojure'i!

Nii et siin on meil sumfunktsioon, mis saab arvväärtuste vektori. recurHüppab tagasi loopkuni saame vektori tühi (meie rekursioon base case). Iga "iteratsiooni" korral lisame väärtuse totalakumulaatorile.

Rekursiooniga säilitame oma muutujadmuutumatu.

Tähelepanek : jah! Saame reduceseda funktsiooni rakendada. Seda näeme Higher Order Functionsteemast.

Samuti on väga levinud objekti lõpliku oleku ülesehitamine . Kujutage ette, et meil on string ja me tahame selle stringi teisendada a-ks url slug.

Ruby'is asuvas OOP-s loome klassi, ütleme nii, et UrlSlugify. Ja sellel klassil on slugify!meetod stringi sisendi teisendamiseks a-ks url slug.

Ilus! See on rakendatud! Siin on meil kohustuslik programmeerimine, öeldes täpselt, mida me tahame igas slugifyprotsessis teha - kõigepealt väiketähed, seejärel eemaldage kasutud tühikud ja lõpuks asendage ülejäänud tühikud sidekriipsudega.

Kuid me muteerime selles protsessis sisendi olekut.

Selle mutatsiooniga saame hakkama funktsiooni koostamise või funktsiooni aheldamise abil. Teisisõnu kasutatakse funktsiooni tulemust järgmise funktsiooni sisendina, muutmata algset sisestusstringi.

Siin on meil:

  • trim: eemaldab tühiku stringi mõlemast otsast
  • lower-case: teisendab stringi väiketähtedeks
  • replace: asendab kõik vaste esinemised antud stringi asendustega

Ühendame kõik kolm funktsiooni ja saame "slugify"oma stringi.

Rääkides funktsioonide kombineerimisest , saame compfunktsiooni kasutada kõigi kolme funktsiooni koostamiseks. Heidame pilgu:

Referentsiaalne läbipaistvus

Rakendame järgmist square function:

Sellel (puhtal) funktsioonil on alati sama väljund, arvestades sama sisendit.

Tahte parameetrina "2" edastamine square functiontagastab alati 4. Nii et nüüd saame asendada (square 2)tähega 4. See on kõik! Meie funktsioon on referentially transparent.

Põhimõtteliselt, kui funktsioon annab sama sisendi jaoks pidevalt sama tulemuse, on see referentsiaalselt läbipaistev.

puhtad funktsioonid + muutumatud andmed = referentsiaalne läbipaistvus

Selle kontseptsiooni puhul on lahe asi, mida saame teha, funktsioon üles märkida. Kujutage ette, et meil on see funktsioon:

(+ 5 8)Võrdub 13. Selle funktsiooni tulemuseks on alati 13. Nii et saame seda teha:

Ja selle väljenduse tulemuseks on alati 16. Võime kogu avaldise asendada arvkonstandiga ja selle memodeerida.

Toimib esmaklassiliste üksustena

Idee toimib esimese klassi üksused on see, et funktsioone ka käsitleda väärtused ja kasutada andmeid.

Clojure'is kasutatakse seda tavaliselt defnfunktsioonide määratlemiseks, kuid see on lihtsalt süntaktiline suhkur (def foo (fn ...)). fntagastab funktsiooni ise. defntagastab varfunktsiooni objektile osutava a .

Funktsioonid esmaklassiliste üksustena võivad:

  • viidata sellele konstantide ja muutujate järgi
  • edastage see parameetrina teistele funktsioonidele
  • tagastage see muudest funktsioonidest tulenevalt

Idee on käsitleda funktsioone väärtustena ja edastada funktsioone nagu andmeid. Nii saame kombineerida erinevaid funktsioone, et luua uusi funktsioone uue käitumisega.

Kujutage ette, et meil on funktsioon, mis võtab kokku kaks väärtust ja seejärel kahekordistab väärtuse. Midagi sellist:

Nüüd funktsioon, mis lahutab väärtused ja tagastab topelt:

Nendel funktsioonidel on sarnane loogika, kuid erinevus seisneb operaatorite funktsioonides. Kui suudame funktsioone käsitleda väärtustena ja edastada neid argumentidena, saame ehitada funktsiooni, mis võtab vastu operaatorfunktsiooni, ja kasutada seda oma funktsiooni sees. Ehitame selle üles!

Valmis! Nüüd on meil fargument ja kasutage seda töötlemiseks aja b. Me sooritanud +ja -funktsioonid koostada koos double-operatorfunktsiooni ja luua uusi käitumist.

Kõrgemat järku funktsioonid

Kõrgemat järku funktsioonidest rääkides peame silmas funktsiooni, mis kas:

  • võtab argumentidena ühe või mitu funktsiooni või
  • tagastab tulemuseks funktsiooni

Eespool double-operatorrakendatud funktsioon on kõrgemat järku funktsioon, kuna see võtab argumendina operaatori funktsiooni ja kasutab seda.

Te olete ilmselt juba kuulnud filter, mapja reduce. Vaatame neid.

Filtreeri

Arvestades kogu, tahame filtreerida atribuudi järgi. Filtrifunktsioon eeldab, et väärtus truevõi falseväärtus määrab, kas element peaks tulemuste kogusse kuuluma või mitte . Põhimõtteliselt, kui tagasihelistamise avaldis on true, sisaldab filtrifunktsioon tulemuste kogu elementi. Vastasel juhul see nii ei ole.

Lihtne näide on see, kui meil on täisarvude kogu ja me tahame ainult paarisarvusid.

Imperatiivne lähenemine

Hädavajalik viis Javascripti abil on:

  • luua tühi vektor evenNumbers
  • itereeruma üle numbersvektori
  • lükake paarisarvud evenNumbersvektorini

filterFunktsiooni saamiseks võime kasutada kõrgemat järku funktsiooni even?ja tagastada paarisarvude loendi:

Üks huvitav probleem, mille Hacker Rank FP Pathil lahendasin, oli Filter Array probleem . Probleemi idee on filtreerida etteantud täisarvude massiiv ja väljastada ainult need väärtused, mis on väiksemad kui määratud väärtus X.

Selle probleemi hädavajalik Javascripti lahendus on umbes järgmine:

Me ütleme täpselt, mida meie funktsioon peab tegema - itereerige kogu üle, võrrelge kogu praegust üksust xja lükake see element resultArraytingimusele.

Deklaratiivne lähenemine

Kuid me tahame selle probleemi lahendamiseks deklaratiivsemat viisi ja kasutada ka filterkõrgema järgu funktsiooni.

Deklareeriv Clojure lahendus oleks umbes selline:

See süntaks tundub esiteks veidi kummaline, kuid on kergesti mõistetav.

#(> x%) on lihtsalt anonüümne funktsioon, mis saab es x ja võrdleb seda kollektsiooni iga elemendiga n. % tähistab anonüümse funktsiooni parameetrit - antud juhul praegune element t he filteri sees .

Saame seda teha ka kaartidega. Kujutage ette, et meil on inimeste kaart nende nameja -ga age. Ja me tahame filtreerida ainult üle vanuse ületanud inimesi, selles näites üle 21-aastaseid inimesi.

Koodi kokkuvõte:

  • meil on nimekiri inimestest (koos nameja age).
  • meil on anonüümne funktsioon #(< 21 (:age %)). Pidage meeles, et t he% tähistab kogu praegust elementi? Noh, kollektsiooni element on inimeste kaart. Kui me do (:age {:name "TK" :age 26}), tagastab see e,sel juhul vanuse väärtuse 26.
  • selle anonüümse funktsiooni alusel filtreerime kõik inimesed.

Kaart

Kaardi idee on muuta kogu.

mapMeetod muudab kogumik rakendades funktsiooni kõik selle elemendid ja ehitada uus kollektsioon tagastatud väärtusi.

Saame peopleülaltoodud sama kollektsiooni. Me ei taha praegu üle vanuse filtreerida. Me tahame lihtsalt stringide loendit, midagi sellist TK is 26 years old. Nii lõplik string võib olla :name is :age years oldkus :nameja :ageon atribuutide iga elemendi peoplekogumise.

Javascripti kohustuslikul viisil oleks see:

Deklareeriva Clojure'i viisil oleks see:

Kogu idee on muuta antud kollektsioon uueks kollektsiooniks.

Teine huvitav häkkeriasetuste probleem oli värskenduste loendi probleem . Tahame lihtsalt värskendada antud kollektsiooni väärtusi nende absoluutväärtustega.

Näiteks sisend [1 2 3 -4 5]vajab väljundit [1 2 3 4 5]. Absoluutväärtus -4on 4.

Lihtne lahendus oleks iga kollektsiooni väärtuse kohapealne värskendus.

Kasutame Math.absfunktsiooni, et teisendada väärtus absoluutväärtuseks ja teha kohapealne värskendus.

See ei ole funktsionaalne viis selle lahenduse rakendamiseks.

Esiteks õppisime muutumatust. Me teame, kui muutumatu on oluline, et muuta meie funktsioonid järjepidevamaks ja prognoositavamaks. Idee on ehitada uus kogu absoluutsete väärtustega kogu.

Teiseks, miks mitte kasutada mapsiin kõigi andmete "teisendamiseks"?

Minu esimene idee oli ehitada to-absolutefunktsioon, mis töötaks ainult ühte väärtust.

Kui see on negatiivne, tahame selle teisendada positiivseks väärtuseks (absoluutväärtuseks). Muidu pole meil vaja seda ümber kujundada.

Nüüd, kui teame, kuidas absoluteühe väärtuse jaoks teha, saame seda funktsiooni kasutada funktsiooni argumendina edastamiseks map. Kas mäletate, et a higher order functionsaab funktsiooni argumendina vastu võtta ja seda kasutada? Jah, kaart saab sellega hakkama!

Vau. Nii ilus! ?

Vähenda

Redutseerimise mõte on saada funktsioon ja kogu ning tagastada üksuste ühendamisel loodud väärtus.

Levinud näide, millest inimesed räägivad, on tellimuse kogusumma saamine. Kujutage ette, et olete ostude veebisaidil. Lisasite Product 1, Product 2, Product 3, ja Product 4teie ostukorv (järjekorras). Nüüd tahame arvutada ostukorvi kogusumma.

Kohustuslikul viisil kordame tellimuste loendi ja summeerime iga toote summa kokku.

Kasutades reducesaame üles ehitada funktsiooni, mis käsitleb funktsiooni amount sumja edastab selle argumendina reducefunktsioonile.

Siin on meil shopping-cartfunktsioon, sum-amountmis võtab vastu voolu total-amount, ja current-productobjekt sumneile.

get-total-amountFunktsiooni kasutatakse abil ning lähtudes .reduceshopping-cartsum-amount0

Teine võimalus kogusumma saamiseks on koostamine mapja reduce. Mida ma sellega öelda tahan? Me saame mapselle abil muuta väärtuste shopping-cartkogumiks amountja seejärel lihtsalt reducefunktsiooni +funktsiooniga kasutada.

Vastu get-amountsaab tooteobjekti ja tagastab ainult amountväärtuse. Mis meil siin on, see on [10 30 20 60]. Ja siis reduceühendab kõik üksused kokku liites. Ilus!

Heitsime pilgu sellele, kuidas toimivad kõik kõrgema järgu funktsioonid. Tahan teile näidata näite, kuidas saame lihtsa näite abil koostada kõik kolm funktsiooni.

Rääkides shopping cart, kujutage ette, et meie tellimusel on see toodete loend:

Soovime, et meie ostukorvis oleksid kõik raamatud kokku. Nii lihtne. Algoritm?

  • filtreerida raamatu tüübi järgi
  • muundage ostukorv kaardi abil kogumikuks
  • ühendage kõik üksused, liites need redutseerimisega

Valmis! ?

Ressursid

Olen korraldanud mõned ressursid, mida lugesin ja uurisin. Jagan neid, mis tundusid mulle väga huvitavad. Ressursside saamiseks külastage minu funktsionaalse programmeerimise Githubi hoidlat .

  • Rubiini spetsiifilised ressursid
  • Javascripti spetsiifilised ressursid
  • Clojure spetsiifilised ressursid

Sissejuhatus

  • FP õppimine JS-is
  • Tutvuge FP-ga Pythoniga
  • FP ülevaade
  • Kiire sissejuhatus funktsionaalsesse JS-i
  • Mis on FP?
  • Funktsionaalne programmeerimise termin

Puhtad funktsioonid

  • Mis on puhas funktsioon?
  • Puhas funktsionaalne programmeerimine 1
  • Puhas funktsionaalne programmeerimine 2

Muutumatud andmed

  • Muutumatu DS funktsionaalseks programmeerimiseks
  • Miks jagatud muutuv olek on kõige kurja juur?
  • Struktuurne jagamine Clojure'is: 1. osa
  • Struktuurne jagamine Clojure'is: 2. osa
  • Struktuurne jagamine Clojure'is: 3. osa
  • Struktuurne jagamine Clojure'is: viimane osa

Kõrgemat järku funktsioonid

  • Kõnekas JS: kõrgema järgu funktsioonid
  • Lõbus lõbus funktsioon Filter
  • Lõbus lõbus funktsioon Kaart
  • Lõbus lõbus funktsioon Põhiline vähendamine
  • Lõbus lõbus funktsioon Täpsem vähendamine
  • Clojure kõrgema järgu funktsioonid
  • Puhtfunktsioonifilter
  • Puhtalt funktsionaalne kaart
  • Puhtalt funktsionaalne vähendamine

Deklaratiivne programmeerimine

  • Deklaratiivne programmeerimine vs hädavajalik

See selleks!

Hei inimesed, ma loodan, et teil oli seda postitust lugedes lõbus ja ma loodan, et õppisite siin palju! See oli minu katse jagada õpitut.

Siin on kõigi selle artikli koodide hoidla .

Tule koos minuga õppima. Jagan ressursse ja oma koodi selles õpifunktsioonide programmeerimise hoidlas .

Loodan, et nägite siin midagi teile kasulikku. Ja näeme järgmine kord! :)

Minu Twitter ja Github. ☺

TK.