En introduktion til de grundlæggende principper for funktionel programmering

Efter lang tid at lære og arbejde med objektorienteret programmering tog jeg et skridt tilbage for at tænke på systemets kompleksitet.

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

Ved at undersøge noget fandt jeg funktionelle programmeringskoncepter som uforanderlighed og ren funktion. Disse begreber er store fordele ved at opbygge bivirkningsfrie funktioner, så det er lettere at vedligeholde systemer - med nogle andre fordele.

I dette indlæg vil jeg fortælle dig mere om funktionel programmering og nogle vigtige begreber med mange kodeeksempler.

Denne artikel bruger Clojure som et eksempel på et programmeringssprog til at forklare funktionel programmering. Hvis du ikke er fortrolig med et LISP-type sprog, offentliggjorde jeg også det samme indlæg i JavaScript. Se: Funktionelle programmeringsprincipper i Javascript

Hvad er funktionel programmering?

Funktionel programmering er et programmeringsparadigme - en stil til opbygning af strukturen og elementerne i computerprogrammer - der behandler beregning som evaluering af matematiske funktioner og undgår skiftende tilstand og ændrede data - Wikipedia

Rene funktioner

Det første grundlæggende koncept, vi lærer, når vi vil forstå funktionel programmering, er rene funktioner . Men hvad betyder det egentlig? Hvad gør en funktion ren?

Så hvordan ved vi, om en funktion er pureeller ej? Her er en meget streng definition af renhed:

  • Det returnerer det samme resultat, hvis det gives de samme argumenter (det kaldes også deterministic)
  • Det forårsager ingen observerbare bivirkninger

Det returnerer det samme resultat, hvis de får de samme argumenter

Forestil dig, at vi vil implementere en funktion, der beregner arealet af en cirkel. En uren funktion modtager radiussom parameter og beregner derefter radius * radius * PI. I Clojure kommer operatøren først, så radius * radius * PIbliver (* radius radius PI):

Hvorfor er dette en uren funktion? Simpelthen fordi det bruger et globalt objekt, der ikke blev sendt som en parameter til funktionen.

Forestil dig nu, at nogle matematikere hævder, at PIværdien faktisk er 42og ændrer værdien af ​​det globale objekt.

Vores urene funktion vil nu resultere i 10 * 10 * 42= 4200. For den samme parameter ( radius = 10) har vi et andet resultat. Lad os ordne det!

TA-DA?! Nu sender vi altid P- I værdien som en parameter til funktionen. Så nu har vi bare adgang til parametre, der sendes til funktionen. Nej external object.

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

Læsning af filer

Hvis vores funktion læser eksterne filer, er det ikke en ren funktion - filens indhold kan ændre sig.

Tilfældig talgenerering

Enhver funktion, der er afhængig af en tilfældig talgenerator, kan ikke være ren.

Det forårsager ingen observerbare bivirkninger

Eksempler på observerbare bivirkninger inkluderer ændring af et globalt objekt eller en parameter, der sendes som reference.

Nu vil vi implementere en funktion for at modtage en heltal og returnere værdien øget med 1.

Vi har counterværdien. Vores urene funktion modtager den værdi og tildeler tælleren igen med værdien øget med 1.

Observation : mutabilitet frarådes i funktionel programmering.

Vi ændrer det globale objekt. Men hvordan skulle vi klare det pure? Bare returner værdien steget med 1. Simpel som det.

Se at vores rene funktion increase-counterreturnerer 2, men counterværdien er stadig den samme. Funktionen returnerer den inkrementerede værdi uden at ændre variabelens værdi.

Hvis vi følger disse to enkle regler, bliver det lettere at forstå vores programmer. Nu er hver funktion isoleret og ude af stand til at påvirke andre dele af vores system.

Rene funktioner er stabile, konsistente og forudsigelige. Med de samme parametre vil rene funktioner altid returnere det samme resultat. Vi behøver ikke at tænke på situationer, hvor den samme parameter har forskellige resultater - fordi det aldrig vil ske.

Fordele ved rene funktioner

Koden er bestemt lettere at teste. Vi behøver ikke at spotte noget. Så vi kan teste rene funktioner med forskellige sammenhænge:

  • Givet en parameter A→ forvent, at funktionen returnerer værdiB
  • Givet en parameter C→ forvent, at funktionen returnerer værdiD

Et simpelt eksempel ville være en funktion til at modtage en samling af numre og forvente, at den forøger hvert element i denne samling.

Vi modtager numberssamlingen, bruger sammen mapmed incfunktionen til at forøge hvert nummer og returnere en ny liste med forøgede numre.

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

Uforanderlighed

Uændret over tid eller kan ikke ændres.

Når data er uforanderlige, kan deres tilstand ikke ændresefter det er oprettet. Hvis du vil ændre et uforanderligt objekt, kan du ikke. I stedet opretter du et nyt objekt med den nye værdi.

I Javascript bruger vi ofte forsløjfen. Denne næste forerklæring har nogle variable variabler.

For hver iteration ændrer vi stateni og sumOfValuestaten . Men hvordan håndterer vi mutabilitet i iteration? Rekursion! Tilbage til Clojure!

Så her har vi den sumfunktion, der modtager en vektor med numeriske værdier. Den recurspringer tilbage i loop, indtil vi får den vektor tom (vores rekursion base case). For hver "iteration" vil vi tilføje værdien til totalakkumulatoren.

Med rekursion beholder vi vores variableruforanderlig.

Observation : Ja! Vi kan bruge reducetil at implementere denne funktion. Vi vil se dette i Higher Order Functionsemnet.

Det er også meget almindeligt at opbygge et objekts endelige tilstand . Forestil dig, at vi har en streng, og vi vil omdanne denne streng til en url slug.

I OOP i Ruby, ville vi skabe en klasse, lad os sige, UrlSlugify. Og denne klasse vil have en slugify!metode til at omdanne strengindgangen til en url slug.

Smuk! Det er implementeret! Her har vi tvingende programmering, der siger nøjagtigt, hvad vi vil gøre i hver slugifyproces - først små bogstaver, fjern derefter ubrugelige hvide rum og til sidst udskift de resterende hvide rum med bindestreger.

Men vi muterer inputtilstanden i denne proces.

Vi kan håndtere denne mutation ved at udføre funktionssammensætning eller funktionskæde. Med andre ord vil resultatet af en funktion blive brugt som input til den næste funktion uden at ændre den originale inputstreng.

Her har vi:

  • trim: fjerner mellemrum fra begge ender af en streng
  • lower-case: konverterer strengen til alle små bogstaver
  • replace: erstatter alle forekomster af match med erstatning i en given streng

Vi kombinerer alle tre funktioner, og vi kan "slugify"strenge vores.

Når vi taler om at kombinere funktioner , kan vi bruge compfunktionen til at komponere alle tre funktioner. Lad os se:

Henvisning til gennemsigtighed

Lad os implementere en square function:

Denne (rene) funktion har altid den samme output, givet den samme input.

At videregive “2” som en parameter for square functionviljen returnerer altid 4. Så nu kan vi erstatte den (square 2)med 4. Det er det! Vores funktion er referentially transparent.

Dybest set, hvis en funktion konsekvent giver det samme resultat for den samme input, er den referentielt gennemsigtig.

rene funktioner + uforanderlige data = referentiel gennemsigtighed

Med dette koncept er en sej ting, vi kan gøre, at huske funktionen. Forestil dig, at vi har denne funktion:

Den (+ 5 8)er lig 13. Denne funktion vil altid resultere i 13. Så vi kan gøre dette:

Og dette udtryk vil altid resultere i 16. Vi kan erstatte hele udtrykket med en numerisk konstant og huske det.

Fungerer som førsteklasses enheder

Idéen med at fungere som førsteklasses enheder er, at funktioner også behandles som værdier og bruges som data.

I Clojure er det almindeligt at bruge defntil at definere funktioner, men dette er bare syntaktisk sukker til (def foo (fn ...)). fnreturnerer selve funktionen. defnreturnerer en varsom peger på et funktionsobjekt.

Fungerer som førsteklasses enheder kan:

  • henvis til det fra konstanter og variabler
  • videregive det som en parameter til andre funktioner
  • returnere det som resultat af andre funktioner

Ideen er at behandle funktioner som værdier og videregive funktioner som data. På denne måde kan vi kombinere forskellige funktioner for at skabe nye funktioner med ny adfærd.

Forestil dig, at vi har en funktion, der summerer to værdier og derefter fordobler værdien. Noget som dette:

Nu er en funktion, der trækker værdier og returnerer det dobbelte:

Disse funktioner har lignende logik, men forskellen er operatorens funktioner. Hvis vi kan behandle funktioner som værdier og videregive disse som argumenter, kan vi opbygge en funktion, der modtager operatørfunktionen og bruge den i vores funktion. Lad os bygge det!

Færdig! Nu har vi et fargument og bruger det til at behandle aog b. Vi passeret +og -funktioner til at komponere med double-operatorfunktion og oprette en ny adfærd.

Funktioner i højere orden

Når vi taler om funktioner af højere orden, mener vi en funktion, der enten:

  • tager en eller flere funktioner som argumenter, eller
  • returnerer en funktion som resultat

Den double-operatorfunktion vi implementeret ovenfor er en højere-ordens funktion, fordi det tager en operatør funktion som argument og bruger det.

Du har sikkert allerede hørt om filter, mapog reduce. Lad os se på disse.

Filter

Givet en samling, vil vi filtrere efter en attribut. Filterfunktionen forventer, at en trueeller en falseværdi bestemmer, om elementet skal eller ikke skal medtages i resultatsamlingen. Grundlæggende, hvis tilbagekaldsudtrykket er true, inkluderer filterfunktionen elementet i resultatsamlingen. Ellers vil det ikke.

Et simpelt eksempel er, når vi har en samling af heltal, og vi kun vil have lige tal.

Imperativ tilgang

En bydende måde at gøre det på med Javascript er at:

  • Opret en tom vektor evenNumbers
  • gentag over numbersvektoren
  • skub lige tal til evenNumbersvektoren

Vi kan bruge filterfunktionen med højere ordre til at modtage even?funktionen og returnere en liste med lige tal:

Et interessant problem, jeg løste på Hacker Rank FP Path, var Filter Array-problemet . Problemideen er at filtrere et givet array af heltal og kun output de værdier, der er mindre end en specificeret værdi X.

En afgørende Javascript-løsning på dette problem er omtrent som:

Vi siger præcis, hvad vores funktion skal gøre - gentag over samlingen, sammenlign samlingens aktuelle vare med x, og skub dette element til, resultArrayhvis det passerer betingelsen.

Deklarativ tilgang

Men vi ønsker en mere erklærende måde at løse dette problem på og ved at bruge den filterhøjere orden også.

En deklarativ Clojure-løsning ville være sådan:

Denne syntaks virker i første omgang lidt underlig, men er let at forstå.

#(> x%) er bare en anonym funktion, der modtager es x og sammenligner den med hvert element i collectio n. % repræsenterer parameteren for den anonyme funktion - i dette tilfælde det aktuelle element inde i he filter.

Vi kan også gøre dette med kort. Forestil dig, at vi har et kort over mennesker med deres nameog age. Og vi vil kun filtrere mennesker over en bestemt alder, i dette eksempel folk, der er mere end 21 år gamle.

Resume af kode:

  • vi har en liste over mennesker (med nameog age).
  • vi har den anonyme funktion #(< 21 (:age %)). Husk at t he% repræsenterer det aktuelle element fra samlingen? Elementet i samlingen er et folkekort. Hvis vi do (:age {:name "TK" :age 26}) returnerer det aldersværdien e,26 i dette tilfælde.
  • vi filtrerer alle mennesker baseret på denne anonyme funktion.

Kort

Idéen med kort er at transformere en samling.

Den mapFremgangsmåden transformerer en samling ved at anvende en funktion til alle dens elementer og opbygge en ny samling fra de returnerede værdier.

Lad os få den samme peoplesamling ovenfor. Vi ønsker ikke at filtrere efter "over alder" nu. Vi vil bare have en liste over strenge, noget lignende TK is 26 years old. Så den sidste streng kan være :name is :age years oldhvor :nameog :ageer attributter fra hvert element i peoplesamlingen.

På en bydende Javascript-måde ville det være:

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

Hele ideen er at omdanne en given samling til en ny samling.

Et andet interessant Hacker Rank-problem var problemet med opdateringslisten . Vi vil bare opdatere værdierne for en given samling med deres absolutte værdier.

For eksempel skal input [1 2 3 -4 5]have output for at være [1 2 3 4 5]. Den absolutte værdi af -4er 4.

En simpel løsning ville være en opdatering på stedet for hver indsamlingsværdi.

Vi bruger Math.absfunktionen til at omdanne værdien til dens absolutte værdi og foretage opdateringen på stedet.

Dette er ikke en funktionel måde at implementere denne løsning på.

Først lærte vi om uforanderlighed. Vi ved, hvordan uforanderlighed er vigtig for at gøre vores funktioner mere konsistente og forudsigelige. Ideen er at opbygge en ny samling med alle absolutte værdier.

For det andet, hvorfor ikke bruge mapher til at "transformere" alle data?

Min første idé var at opbygge en to-absolutefunktion, der kun kunne håndtere en værdi.

Hvis det er negativt, vil vi transformere det til en positiv værdi (den absolutte værdi). Ellers behøver vi ikke transformere det.

Nu hvor vi ved, hvordan man gør absolutefor en værdi, kan vi bruge denne funktion til at overføre som et argument til mapfunktionen. Kan du huske, at en higher order functionkan modtage en funktion som et argument og bruge den? Ja, kort kan det!

Wow. Så smuk! ?

Reducere

Idéen med reducere er at modtage en funktion og en samling og returnere en værdi, der er oprettet ved at kombinere elementerne.

Et almindeligt eksempel, som folk taler om, er at få det samlede beløb for en ordre. Forestil dig, at du var på et shoppingwebsted. Du har tilføjet Product 1, Product 2, Product 3, og Product 4til din indkøbskurv (rækkefølge). Nu vil vi beregne det samlede beløb for indkøbskurven.

På afgørende måde gentager vi ordrelisten og summerer hvert produktbeløb til det samlede beløb.

Ved hjælp af reducekan vi opbygge en funktion til at håndtere amount sumog overføre den som et argument til reducefunktionen.

Her har vi shopping-cartden funktion, sum-amountder modtager strømmen total-amount, og current-productobjektet til sumdem.

Den get-total-amountfunktion bruges til reduceden shopping-cartved hjælp af sum-amountog at starte ud fra 0.

En anden måde at få det samlede beløb på er at komponere mapog reduce. Hvad mener jeg med det? Vi kan bruge maptil at omdanne den shopping-carttil en samling af amountværdier og derefter bare bruge reducefunktionen med +funktion.

Modtager get-amountproduktobjektet og returnerer kun amountværdien. Så hvad vi har her er [10 30 20 60]. Og så reducekombinerer alle varer ved at tilføje. Smuk!

Vi kiggede på, hvordan hver højere ordensfunktion fungerer. Jeg vil vise dig et eksempel på, hvordan vi kan komponere alle tre funktioner i et simpelt eksempel.

Når vi taler om shopping cart, forestil os at vi har denne liste over produkter i vores rækkefølge:

Vi ønsker det samlede beløb af alle bøger i vores indkøbskurv. Så simpelt er det. Algoritmen?

  • filtrer efter bogtype
  • omdanne indkøbskurven til en samling af beløb ved hjælp af kort
  • kombinere alle emner ved at tilføje dem sammen med reducer

Færdig! ?

Ressourcer

Jeg har organiseret nogle ressourcer, jeg har læst og studeret. Jeg deler dem, som jeg fandt rigtig interessante. For flere ressourcer, besøg mit funktionelle programmering Github-lager .

  • Rubin-specifikke ressourcer
  • Javascript-specifikke ressourcer
  • Clojure specifikke ressourcer

Introduktion

  • Læring FP i JS
  • Introduktion til FP med Python
  • Oversigt over FP
  • En hurtig introduktion til funktionel JS
  • Hvad er FP?
  • Funktionel programmeringsjargon

Rene funktioner

  • Hvad er en ren funktion?
  • Ren funktionel programmering 1
  • Ren funktionel programmering 2

Uforanderlige data

  • Uforanderlig DS til funktionel programmering
  • Hvorfor delt mutabel tilstand er roden til alt ondt
  • Strukturel deling i Clojure: Del 1
  • Strukturel deling i Clojure: Del 2
  • Strukturel deling i Clojure: Del 3
  • Strukturel deling i Clojure: Afsluttende del

Funktioner i højere orden

  • Veltalende JS: Funktioner til højere ordre
  • Sjov sjov funktion Filter
  • Sjov sjov funktion Kort
  • Sjov sjov funktion Basic Reduce
  • Sjov sjov funktion Avanceret Reducer
  • Funktioner til højere ordre i Clojure
  • Rent filter
  • Rent funktionelt kort
  • Rent funktionel reduktion

Deklarativ programmering

  • Deklarativ programmering vs imperativ

Det er det!

Hej folk, jeg håber, du havde det sjovt ved at læse dette indlæg, og jeg håber, du har lært meget her! Dette var mit forsøg på at dele det, jeg lærer.

Her er arkivet med alle koder fra denne artikel.

Kom og lær med mig. Jeg deler ressourcer og min kode i dette Learning Functional Programming repository .

Jeg håber, du så noget nyttigt for dig her. Og vi ses næste gang! :)

Min Twitter & Github. ☺

TK.