Funktionelle programmeringsprincipper i Javascript

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

”Kompleksitet er noget, der gør software svært at forstå eller at ændre.” - John Outerhout

Ved at undersøge fandt jeg funktionelle programmeringskoncepter som immutabilitet og rene funktioner. Disse koncepter giver dig mulighed for at opbygge funktioner uden sideeffekt, 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 koncepter med en masse kodeeksempler i JavaScript.

Hvad er funktionel programmering?

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

Rene funktioner

“Vanddråbe” af Mohan Murugesan på Unsplash

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 ren eller ej? Her er en meget streng definition af renhed:

  • Det returnerer det samme resultat, hvis de får de samme argumenter (det kaldes også deterministisk)
  • Det medfører 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 radius som parameter og beregner derefter 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 PI-værdien faktisk er 42 og ændrer værdien af ​​det globale objekt.

Vores urene funktion resulterer nu i 10 * 10 * 42 = 4200. For den samme parameter (radius = 10) har vi et andet resultat.

Lad os ordne det!

Nu overfører vi altid værdien af ​​PI som en parameter til funktionen. Så nu får vi bare adgang til parametre, der er videregivet til funktionen. Intet eksternt objekt.

  • For parametrene radius = 10 ogPI = 3.14 vil vi altid have det samme resultat: 314.0
  • For parametrene radius = 10 ogPI = 42 vil 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 ændres.

Tilfældig antal generering

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

Det medfører ingen observerbare bivirkninger

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

Nu vil vi implementere en funktion til at modtage en heltalværdi og returnere værdien øget med 1.

Vi har tællerværdien. Vores uren funktion modtager denne værdi og tildeler tælleren med værdien øget med 1.

Observation: mutabilitet frarådes i funktionel programmering.

Vi ændrer det globale objekt. Men hvordan ville vi gøre det rent? Bare returner værdien steget med 1.

Se, at vores rene funktionsforøgelse returnerer 2, men tællerværdien er stadig den samme. Funktionen returnerer den forøgede værdi uden at ændre variablen.

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

Rene funktioner er stabile, konsistente og forudsigelige. Givet 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 nemmere at teste. Vi behøver ikke håner noget. Så vi kan enhedsteste rene funktioner med forskellige sammenhænge:

  • Givet en parameter A → forventer, at funktionen returnerer værdi B
  • Givet en parameter C → forventer, at funktionen returnerer værdi D

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

Vi modtager nummearrayet, bruger kort til at øge hvert nummer og returnerer en ny liste med inkrementerede numre.

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

uforanderlighed

Uændret over tid eller ikke kan ændres.
“Change neon light signage” af Ross Findon på Unsplash

Når data er uforanderlige, kan deres tilstand ikke ændres, når de 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 for-loop. Dette næste for udsagn har nogle mutable variabler.

For hver iteration ændrer vi tilstanden i og sumOfValue. Men hvordan håndterer vi mutabilitet i iteration? Rekursion.

Så her har vi sumfunktionen, der modtager en vektor af numeriske værdier. Funktionen kalder sig, indtil vi får listen tom (vores rekursionsbase-sag). For hver "iteration" tilføjer vi værdien til den samlede akkumulator.

Med rekursion holder vi vores variabler uforanderlige. Listen og akkumulatorvariablerne ændres ikke. Det holder den samme værdi.

Observation: Vi kan bruge reducere til at implementere denne funktion. Vi vil dække dette i emnerne med højere ordre.

Det er også meget almindeligt at opbygge den endelige tilstand af et objekt. Forestil dig, at vi har en streng, og vi vil omdanne denne streng til en url-snegle.

I Objektorienteret programmering i Ruby, ville vi oprette en klasse, lad os sige UrlSlugify. Og denne klasse har en slugify-metode til at omdanne strengindgangen til en url-slug.

Det er implementeret!

Her har vi tvingende programmering, der siger nøjagtigt, hvad vi vil gøre i hver slugify-proces - først med små bogstaver, derefter fjern ubrugelige hvide rum og til sidst erstatte de resterende hvide rum med bindestreger.

Men vi muterer inputtilstanden i denne proces.

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

Her har vi:

  • toLowerCase: konverterer strengen til alle små bogstaver
  • trim: fjerner hvid plads fra begge ender af en streng
  • split and join: erstatter alle tilfælde af kamp med erstatning i en given streng

Vi kombinerer alle disse 4 funktioner, og vi kan "slugify" vores streng.

Henvisning til gennemsigtighed

“Person, der holder briller” af Josh Calabrese på Unsplash

Lad os implementere en firkantet funktion:

Denne rene funktion vil altid have den samme output, givet den samme input.

At videregive 2 som en parameter for kvadratfunktionen returnerer altid 4. Så nu kan vi erstatte firkanten (2) med 4. Vores funktion er referencemæssigt gennemsigtig.

Grundlæggende, hvis en funktion konsekvent giver det samme resultat for den samme input, er den fortrinsvis gennemsigtig.

rene funktioner + uforanderlige data = referencemæssig gennemsigtighed

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

Og vi kalder det med disse parametre:

Summen (5, 8) er lig med 13. Denne funktion resulterer altid 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.

Funktioner som førsteklasses enheder

“Førsteklasses” af Andrew Neel på Unsplash

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

Funktioner som førsteklasses enheder kan:

  • henvises til det fra konstanter og variabler
  • overfør det som en parameter til andre funktioner
  • returner det som resultat af andre funktioner

Tanken 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 opførsel.

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 operatørens 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.

Nu har vi et f-argument, og bruger det til at behandle a og b. Vi passerede sum- og subtraktionsfunktionerne for at komponere med doubleOperator-funktionen og skabe en ny opførsel.

Funktioner med højere orden

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

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

DoubleOperator-funktionen, vi implementerede ovenfor, er en funktion med højere orden, fordi den tager en operatørfunktion som et argument og bruger den.

Du har sandsynligvis allerede hørt om filter, kort og reduktion. Lad os se på disse.

Filter

Givet en samling, vil vi filtrere efter en attribut. Filterfunktionen forventer en sand eller falsk værdi for at bestemme, om elementet skal eller ikke skal inkluderes i resultatsamlingen. Grundlæggende, hvis tilbagekaldsudtrykket er sandt, inkluderer filterfunktionen elementet i resultatsamlingen. Ellers vil det ikke.

Et simpelt eksempel er, når vi har en samling heltal, og vi kun ønsker de lige tal.

Imperativ tilgang

En vigtig måde at gøre det med JavaScript er at:

  • opret en tom matrix evenNumbers
  • iterere over antal array
  • skub jævne tal til evenNumbers-matrixen

Vi kan også bruge filteret med højere ordre til at modtage den jævne funktion og returnere en liste med lige tal:

Et interessant problem, som jeg løste på Hacker Rank FP-stien, var filter Array-problemet. Problemideen er at filtrere en given række med heltal og kun udskrive de værdier, der er mindre end en specificeret værdi X.

En vigtig JavaScript-løsning på dette problem er noget som:

Vi siger nøjagtigt, hvad vores funktion skal gøre - iterere over samlingen, sammenligne den nuværende samling med x og skub dette element til resultArray, hvis det passerer betingelsen.

Deklarativ tilgang

Men vi ønsker en mere deklarativ måde at løse dette problem på, og vi bruger også filteret med højere ordre.

En deklarativ JavaScript-løsning ville være sådan som denne:

Brug af denne i den mindre funktion virker i første omgang lidt mærkelig, men er let at forstå.

dette vil være den anden parameter i filterfunktionen. I dette tilfælde er 3 (x) repræsenteret af dette. Det er det.

Vi kan også gøre dette med kort. Forestil dig, at vi har et kort over mennesker med deres navn og alder.

Og vi vil kun filtrere personer over en bestemt værdi af alderen, i dette eksempel mennesker, der er mere end 21 år gamle.

Resume af kode:

  • vi har en liste over mennesker (med navn og alder).
  • vi har en funktion ældrehan21. I dette tilfælde ønsker vi for hver person i folkegruppe adgang til alderen og se, om den er ældre end 21 år.
  • vi filtrerer alle mennesker baseret på denne funktion.

Kort

Idéen med kort er at omdanne en samling.

Kortmetoden transformerer en samling ved at anvende en funktion på alle dens elementer og opbygge en ny samling ud fra de returnerede værdier.

Lad os få den samme folkekollektion ovenfor. Vi ønsker ikke at filtrere efter "over alder" nu. Vi vil bare have en liste med strenge, ligesom TK er 26 år gammel. Så den sidste streng kan være: navn er: alder år gammel hvor: navn og: alder er attributter fra hvert element i folkesamlingen.

På en nødvendig JavaScript-måde ville det være:

På en deklarativ JavaScript-måde ville det være:

Hele ideen er at omdanne et givet array til et nyt array.

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

F.eks. Skal input [1, 2, 3, -4, 5], at output er [1, 2, 3, 4, 5]. Den absolutte værdi på -4 er 4.

En enkel løsning ville være en lokal opdatering for hver indsamlingsværdi.

Vi bruger Math.abs-funktionen til at omdanne værdien til dens absolutte værdi og udføre stedet-opdateringen.

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. Tanken er at opbygge en ny samling med alle absolutte værdier.

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

Min første idé var at teste Math.abs-funktionen for kun at håndtere en værdi.

Vi ønsker at omdanne hver værdi til en positiv værdi (den absolutte værdi).

Nu, hvor vi ved, hvordan man gør absolut for en værdi, kan vi bruge denne funktion til at videregive som et argument til kortfunktionen. Kan du huske, at en funktion med højere orden kan modtage en funktion som et argument og bruge den? Ja, kort kan gøre det!

Wow. Så smuk!

Reducere

Ideen med at reducere er at modtage en funktion og en samling og returnere en værdi oprettet ved at kombinere elementerne.

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

På tvingende vis vil vi gentage ordrelisten og summe hvert produktbeløb til det samlede beløb.

Ved hjælp af reducere kan vi opbygge en funktion til at håndtere beløbssummen og videregive den som et argument til reduktionsfunktionen.

Her har vi shoppingCart, funktionen sumAmount, der modtager den aktuelle currentTotalAmount, og ordreobjektet til at summere dem.

Funktionen getTotalAmount bruges til at reducere shoppingCart ved at bruge sumAmount og starte fra 0.

En anden måde at få det samlede beløb på er at komponere kort og reducere. Hvad mener jeg med det? Vi kan bruge kort til at omdanne shoppingCart til en samling af mængdeværdier og derefter bare bruge reduktionsfunktionen med sumAmount-funktionen.

GetAmount modtager produktobjektet og returnerer kun mængdeværdien. Så hvad vi har her, er [10, 30, 20, 60]. Og så kombinerer reduktionen alle elementer ved at tilføje. Smuk!

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

Når vi taler om indkøbskurv, kan du forestille dig, at vi har denne liste over produkter i vores rækkefølge:

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

  • filtrer efter bogtype
  • forvandle indkøbskurven til en samling af beløb ved hjælp af kort
  • kombiner alle elementer ved at tilføje dem med reducere

Færdig!

Ressourcer

Jeg har organiseret nogle ressourcer, jeg har læst og studeret. Jeg deler dem, som jeg syntes virkelig var interessante. For mere ressourcer, kan du besøge mit Github-arkiv til funktionel programmering

  • EcmaScript 6-kursus af Wes Bos
  • JavaScript af OneMonth
  • Rubin-specifikke ressourcer
  • Javascript-specifikke ressourcer
  • Clojure-specifikke ressourcer

introer

  • Læring FP i JS
  • Intro do FP med Python
  • Oversigt over FP
  • En hurtig introduktion til funktionel JS
  • Hvad er FP?
  • Funktionel programmerings jargon

Rene funktioner

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

Uændelige data

  • Uforanderlig DS til funktionel programmering
  • Hvorfor delt mutable tilstand er roden til alt ondt

Funktioner med højere orden

  • Eloquent JS: Højere ordrefunktioner
  • Sjov sjov funktion Filter
  • Sjov sjov funktion Kort
  • Sjov sjov funktion Grundlæggende reduktion
  • Sjov sjov funktion Avanceret Reducer
  • Clojure-funktioner til højere ordre
  • Rent funktionsfilter
  • Rent funktionelt kort
  • Rent funktionel reduktion

Deklarativ programmering

  • Deklarativ programmering vs imperativ

Det er det!

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

Her er depotet med alle koder fra denne artikel.

Kom og lær med mig. Jeg deler ressourcer og min kode i dette læringsfunktionelle programmeringslager.

Jeg skrev også et FP-indlæg, men brugte hovedsageligt Clojure.

Hvis du vil have et komplet Javascript-kursus, lære mere kodningskompetencer i den virkelige verden og opbygge projekter, så prøv en måned Javascript Bootcamp. Vi ses der ☺

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

Jeg håber du kunne lide dette indhold. Støtt mit arbejde med Ko-Fi

Min Twitter & Github.

TK.