En introduksjon til de grunnleggende prinsippene for funksjonell programmering

Etter lang tid med å lære og jobbe med objektorientert programmering, tok jeg et skritt tilbake for å tenke på systemkompleksitet.

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

Gjennom å undersøke, fant jeg funksjonelle programmeringskonsepter som uforanderlighet og ren funksjon. Disse konseptene er store fordeler ved å bygge funksjoner uten bivirkninger, så det er lettere å vedlikeholde systemer - med noen andre fordeler.

I dette innlegget vil jeg fortelle deg mer om funksjonell programmering, og noen viktige konsepter, med mange kodeeksempler.

Denne artikkelen bruker Clojure som et programmeringsspråkeksempel for å forklare funksjonell programmering. Hvis du ikke er komfortabel med et språk av typen LISP, publiserte jeg også det samme innlegget i JavaScript. Ta en titt: Funksjonelle programmeringsprinsipper i Javascript

Hva er funksjonell programmering?

Funksjonell programmering er et programmeringsparadigme - en stil for å bygge strukturen og elementene i dataprogrammer - som behandler beregning som evaluering av matematiske funksjoner og unngår endring av tilstand og foranderlige data - Wikipedia

Rene funksjoner

Det første grunnleggende konseptet vi lærer når vi vil forstå funksjonell programmering, er rene funksjoner . Men hva betyr det egentlig? Hva gjør en funksjon ren?

Så hvordan vet vi om en funksjon er pureeller ikke? Her er en veldig streng definisjon av renhet:

  • Den returnerer det samme resultatet hvis de får de samme argumentene (det blir også referert til som deterministic)
  • Det gir ingen observerbare bivirkninger

Det gir samme resultat hvis de får de samme argumentene

Tenk deg at vi vil implementere en funksjon som beregner arealet til en sirkel. En uren funksjon vil motta radiussom parameter, og deretter beregne radius * radius * PI. I Clojure kommer operatøren først, så radius * radius * PIblir (* radius radius PI):

Hvorfor er dette en uren funksjon? Rett og slett fordi den bruker et globalt objekt som ikke ble sendt som en parameter til funksjonen.

Tenk deg at noen matematikere hevder at PIverdien faktisk er 42og endrer verdien til det globale objektet.

Vår urene funksjon vil nå resultere i 10 * 10 * 42= 4200. For den samme parameteren ( radius = 10) har vi et annet resultat. La oss ordne det!

TA-DA?! Nå sender vi alltid P- I verdien som en parameter til funksjonen. Så nå har vi bare tilgang til parametere sendt til funksjonen. Ingen external object.

  • For parametrene radius = 10& PI = 3.14vil vi alltid ha det samme resultatet:314.0
  • For parametrene radius = 10& PI = 42vil vi alltid ha det samme resultatet:4200

Lese filer

Hvis funksjonen vår leser eksterne filer, er det ikke en ren funksjon - filens innhold kan endres.

Tilfeldig antallgenerering

Enhver funksjon som er avhengig av en tilfeldig tallgenerator kan ikke være ren.

Det gir ingen observerbare bivirkninger

Eksempler på observerbare bivirkninger inkluderer modifisering av et globalt objekt eller en parameter sendt ved referanse.

Nå vil vi implementere en funksjon for å motta et heltall og returnere verdien økt med 1.

Vi har counterverdien. Vår urene funksjon mottar den verdien og tildeler telleren på nytt med verdien økt med 1.

Observasjon : mutabilitet frarådes i funksjonell programmering.

Vi modifiserer det globale objektet. Men hvordan skulle vi klare det pure? Bare returner verdien økt med 1. Enkelt som det.

Se at vår rene funksjon increase-counterreturnerer 2, men counterverdien er fortsatt den samme. Funksjonen returnerer den økte verdien uten å endre verdien på variabelen.

Hvis vi følger disse to enkle reglene, blir det lettere å forstå programmene våre. Nå er hver funksjon isolert og ikke i stand til å påvirke andre deler av systemet vårt.

Rene funksjoner er stabile, konsistente og forutsigbare. Gitt de samme parameterne, vil rene funksjoner alltid gi samme resultat. Vi trenger ikke tenke på situasjoner når den samme parameteren har forskjellige resultater - fordi det aldri vil skje.

Ren funksjoner fordeler

Koden er definitivt lettere å teste. Vi trenger ikke å spotte noe. Så vi kan teste rene funksjoner med forskjellige sammenhenger:

  • Gitt en parameter A→ forvent funksjonen skal returnere verdiB
  • Gitt en parameter C→ forvent funksjonen skal returnere verdiD

Et enkelt eksempel vil være en funksjon for å motta en samling av tall og forvente at den øker hvert element i denne samlingen.

Vi mottar numberssamlingen, bruker mapmed incfunksjonen til å øke hvert nummer, og returnere en ny liste over trinn.

For den input[1 2 3 4 5]forventede outputville være [2 3 4 5 6].

Uforanderlighet

Uendret over tid eller kan ikke endres.

Når data er uforanderlige, kan tilstanden ikke endresetter at den er opprettet. Hvis du vil endre et uforanderlig objekt, kan du ikke. I stedet oppretter du et nytt objekt med den nye verdien.

I Javascript bruker vi ofte forløkken. Denne neste foruttalelsen har noen mutable variabler.

For hver iterasjon endrer ivi sumOfValuestaten og staten . Men hvordan håndterer vi mutabilitet i iterasjon? Rekursjon! Tilbake til Clojure!

Så her har vi sumfunksjonen som mottar en vektor med numeriske verdier. Den recurhopper tilbake til loopfør vi får vektoren tom (vår rekursjon base case). For hver "iterasjon" vil vi legge til verdien i totalakkumulatoren.

Med rekursjon holder vi variablene våreuforanderlig.

Observasjon : Ja! Vi kan bruke reducetil å implementere denne funksjonen. Vi vil se dette i Higher Order Functionsemnet.

Det er også veldig vanlig å bygge opp den endelige tilstanden til et objekt. Tenk deg at vi har en streng, og vi vil transformere denne strengen til en url slug.

I OOP i Ruby, ville vi lage en klasse, la oss si UrlSlugify,. Og denne klassen vil ha en slugify!metode for å transformere strenginngangen til en url slug.

Vakker! Det er implementert! Her har vi tvingende programmering som sier nøyaktig hva vi vil gjøre i hver slugifyprosess - først små bokstaver, fjern deretter ubrukelige hvite mellomrom og til slutt erstatt gjenværende hvite mellomrom med bindestreker.

Men vi muterer inngangstilstanden i denne prosessen.

Vi kan håndtere denne mutasjonen ved å gjøre funksjonssammensetning, eller funksjonskjetting. Resultatet av en funksjon vil med andre ord bli brukt som inngang for neste funksjon, uten å endre den originale inngangsstrengen.

Her har vi:

  • trim: fjerner mellomrom fra begge ender av en streng
  • lower-case: konverterer strengen til alle små bokstaver
  • replace: erstatter alle forekomster av kamp med erstatning i en gitt streng

Vi kombinerer alle tre funksjonene, og vi kan "slugify"strengen vår.

Når vi snakker om å kombinere funksjoner , kan vi bruke compfunksjonen til å komponere alle tre funksjonene. La oss ta en titt:

Referansetransparens

La oss implementere en square function:

Denne (rene) funksjonen vil alltid ha samme utgang, gitt samme inngang.

Å sende "2" som en parameter for square functionviljen returnerer alltid 4. Så nå kan vi erstatte den (square 2)med 4. Det er det! Vår funksjon er referentially transparent.

I utgangspunktet, hvis en funksjon konsekvent gir det samme resultatet for samme inngang, er den referentielt gjennomsiktig.

rene funksjoner + uforanderlige data = referansetransparens

Med dette konseptet er en kul ting vi kan gjøre å huske funksjonen. Tenk deg at vi har denne funksjonen:

Det (+ 5 8)samme 13. Denne funksjonen vil alltid resultere i 13. Så vi kan gjøre dette:

Og dette uttrykket vil alltid resultere i 16. Vi kan erstatte hele uttrykket med en numerisk konstant og huske det.

Fungerer som førsteklasses enheter

Ideen om å fungere som førsteklasses enheter er at funksjoner også blir behandlet som verdier og brukt som data.

I Clojure er det vanlig å bruke for defnå definere funksjoner, men dette er bare syntaktisk sukker for (def foo (fn ...)). fnreturnerer selve funksjonen. defnreturnerer en varsom peker på et funksjonsobjekt.

Fungerer som førsteklasses enheter kan:

  • referer til det fra konstanter og variabler
  • gi den som parameter til andre funksjoner
  • returnere den som resultat fra andre funksjoner

Ideen er å behandle funksjoner som verdier og overføre funksjoner som data. På denne måten kan vi kombinere forskjellige funksjoner for å skape nye funksjoner med ny oppførsel.

Tenk deg at vi har en funksjon som summerer to verdier og deretter dobler verdien. Noe sånt som dette:

Nå en funksjon som trekker verdier og returnerer doblet:

Disse funksjonene har lignende logikk, men forskjellen er operatørfunksjonene. Hvis vi kan behandle funksjoner som verdier og sende disse som argumenter, kan vi bygge en funksjon som mottar operatørfunksjonen og bruke den i vår funksjon. La oss bygge den!

Ferdig! Nå har vi et fargument, og bruker det til å behandle aog b. Vi passerte +og -funksjoner for å komponere med double-operatorfunksjon og opprette en ny atferd.

Funksjoner med høyere ordre

Når vi snakker om funksjoner av høyere orden, mener vi en funksjon som enten:

  • tar en eller flere funksjoner som argumenter, eller
  • returnerer en funksjon som resultatet

Den double-operatorfunksjon vi implementert ovenfor er en høyere-ordens funksjon fordi det tar en operatør funksjon som et argument og bruker det.

Du har sikkert allerede hørt om filter, mapog reduce. La oss ta en titt på disse.

Filter

Gitt en samling, vil vi filtrere etter et attributt. Filterfunksjonen forventer at en trueeller falseverdi bestemmer om elementet skal eller ikke skal inkluderes i resultatsamlingen. I utgangspunktet, hvis tilbakeringingsuttrykket er true, vil filterfunksjonen inkludere elementet i resultatsamlingen. Ellers vil det ikke.

Et enkelt eksempel er når vi har en samling med heltall, og vi vil bare ha partallene.

Imperativ tilnærming

En viktig måte å gjøre det med Javascript på er å:

  • lage en tom vektor evenNumbers
  • iterere over numbersvektoren
  • skyv partallene til evenNumbersvektoren

Vi kan bruke funksjonen for filterhøyere ordre for å motta even?funksjonen, og returnere en liste med partallnumre:

Et interessant problem jeg løste på Hacker Rank FP Path var Filter Array-problemet . Problemideen er å filtrere et gitt utvalg med heltall og bare skrive ut de verdiene som er mindre enn en spesifisert verdi X.

En viktig Javascript-løsning på dette problemet er omtrent som:

Vi sier nøyaktig hva funksjonen vår trenger å gjøre - iterere over samlingen, sammenligne samlingens nåværende vare med x, og skyv dette elementet til resultArrayhvis det passerer tilstanden.

Deklarativ tilnærming

Men vi vil ha en mer deklarativ måte å løse dette problemet på, og å bruke filterhøyere ordensfunksjon også.

En deklarativ Clojure-løsning vil være omtrent slik:

Denne syntaksen virker i utgangspunktet litt rart, men er lett å forstå.

#(> x%) er bare en anonym funksjon som mottar ex og sammenligner den med hvert element i samlingen n. % representerer parameteren til den anonyme funksjonen - i dette tilfellet det nåværende elementet i t he filter.

Vi kan også gjøre dette med kart. Tenk deg at vi har et kart over mennesker med deres nameog age. Og vi ønsker å filtrere bare personer over en spesifikk verdi av alder, i dette eksemplet personer som er mer enn 21 år gamle.

Sammendrag av kode:

  • vi har en liste over mennesker (med nameog age).
  • vi har den anonyme funksjonen #(< 21 (:age %)). Husk at t he% representerer det nåværende elementet fra samlingen? Elementet i samlingen er et folkekart. Hvis vi do (:age {:name "TK" :age 26}), returnerer den aldersverdien e,26 i dette tilfellet.
  • vi filtrerer alle mennesker basert på denne anonyme funksjonen.

Kart

Ideen med kart er å transformere en samling.

Den mapmetode transformerer en samling ved å anvende en funksjon til alle dens elementer og bygge en ny samling fra de returnerte verdiene.

La oss få den samme peoplesamlingen ovenfor. Vi ønsker ikke å filtrere etter "over alder" nå. Vi vil bare ha en liste over strenger, noe sånt som TK is 26 years old. Så den endelige strengen kan være :name is :age years oldhvor :nameog :ageer attributter fra hvert element i peoplesamlingen.

På en tvingende Javascript-måte ville det være:

På en erklærende Clojure-måte ville det være:

Hele ideen er å transformere en gitt samling til en ny samling.

Et annet interessant Hacker Rank-problem var oppdateringslisteproblemet . Vi vil bare oppdatere verdiene til en gitt samling med deres absolutte verdier.

For eksempel [1 2 3 -4 5]trenger inngangen at utgangen skal være [1 2 3 4 5]. Den absolutte verdien av -4er 4.

En enkel løsning vil være en oppdatering på stedet for hver samlingsverdi.

Vi bruker Math.absfunksjonen til å transformere verdien til den absolutte verdien, og gjør oppdateringen på stedet.

Dette er ikke en funksjonell måte å implementere denne løsningen på.

Først lærte vi om uforanderlighet. Vi vet hvordan uforanderlighet er viktig for å gjøre funksjonene våre mer konsistente og forutsigbare. Tanken er å bygge en ny samling med alle absolutte verdier.

For det andre, hvorfor ikke bruke mapher til å "transformere" all data?

Min første idé var å bygge en to-absolutefunksjon for å håndtere bare en verdi.

Hvis det er negativt, vil vi transformere det til en positiv verdi (den absolutte verdien). Ellers trenger vi ikke å transformere det.

Nå som vi vet hvordan vi skal gjøre absolutefor en verdi, kan vi bruke denne funksjonen til å overføre som et argument til mapfunksjonen. Husker du at a higher order functionkan motta en funksjon som et argument og bruke den? Ja, kart kan gjøre det!

Wow. Så vakker! ?

Redusere

Ideen om å redusere er å motta en funksjon og en samling, og returnere en verdi skapt ved å kombinere elementene.

Et vanlig eksempel folk snakker om er å få totalbeløpet for en ordre. Tenk deg at du var på et shoppingnettsted. Du har lagt til Product 1, Product 2, Product 3, og Product 4til i handlekurven (rekkefølge). Nå ønsker vi å beregne totalbeløpet på handlekurven.

På tvingende måte vil vi gjenta ordrelisten og summere hvert produktbeløp til det totale beløpet.

Ved hjelp av reducekan vi bygge en funksjon for å håndtere amount sumog overføre den som et argument til reducefunksjonen.

Her har vi shopping-cart, funksjonen sum-amountsom mottar strømmen total-amount, og current-productobjektet til sumdem.

Den get-total-amountfunksjonen brukes til reduceden shopping-cartved hjelp av sum-amountog med start fra 0.

En annen måte å få totalbeløpet på er å komponere mapog reduce. Hva mener jeg med det? Vi kan bruke maptil å transformere shopping-carttil en samling amountverdier, og deretter bare bruke reducefunksjonen med +funksjon.

Den get-amountmottar produktet objektet og returnerer bare den amountverdien. Så det vi har her er [10 30 20 60]. Og så reducekombinerer alle elementene ved å legge sammen. Vakker!

Vi tok en titt på hvordan hver høyere ordensfunksjon fungerer. Jeg vil vise deg et eksempel på hvordan vi kan komponere alle tre funksjonene i et enkelt eksempel.

Når du snakker om shopping cart, forestill deg at vi har denne listen over produkter i vår rekkefølge:

Vi ønsker den totale mengden av alle bøker i handlekurven. Så enkelt som det. Algoritmen?

  • filtrer etter boktype
  • forvandle handlekurven til en mengde samling ved hjelp av kart
  • kombinere alle elementene ved å legge dem sammen med redusere

Ferdig! ?

Ressurser

Jeg har organisert noen ressurser jeg har lest og studert. Jeg deler de som jeg syntes var veldig interessante. For flere ressurser, besøk min funksjonelle programmering Github repository .

  • Ruby-spesifikke ressurser
  • Javascript-spesifikke ressurser
  • Clojure spesifikke ressurser

Intro

  • Lære FP i JS
  • Intro do FP with Python
  • Oversikt over FP
  • En rask intro til funksjonell JS
  • Hva er FP?
  • Funksjonell programmeringssjargong

Rene funksjoner

  • Hva er en ren funksjon?
  • Ren funksjonell programmering 1
  • Ren funksjonell programmering 2

Uforanderlige data

  • Uforanderlig DS for funksjonell programmering
  • Hvorfor delt foranderlig tilstand er roten til alt ondt
  • Strukturell deling i Clojure: Del 1
  • Strukturell deling i Clojure: Del 2
  • Strukturell deling i Clojure: Del 3
  • Strukturell deling i Clojure: Avsluttende del

Funksjoner med høyere ordre

  • Veltalende JS: Funksjoner med høyere ordre
  • Morsom morsom funksjon Filter
  • Morsom morsom funksjon Kart
  • Morsom morsom funksjon Basic Reduce
  • Morsom morsom funksjon Advanced Reduce
  • Funksjoner for høyere ordre i Clojure
  • Rent funksjonsfilter
  • Rent funksjonelt kart
  • Rent funksjonell reduksjon

Deklarativ programmering

  • Deklarativ programmering vs imperativ

Det er det!

Hei folkens, jeg håper du hadde det gøy å lese dette innlegget, og jeg håper du har lært mye her! Dette var mitt forsøk på å dele det jeg lærer.

Her er depotet med alle koder fra denne artikkelen.

Kom og lær med meg. Jeg deler ressurser og koden min i dette Learning Functional Programming repository .

Jeg håper du så noe som var nyttig for deg her. Og vi sees neste gang! :)

Min Twitter og Github. ☺

TK.