En funktionel programmers introduktion til JavaScript (komponering af software)

Smoke Art Cubes to Smoke - MattysFlicks - (CC BY 2.0)
Bemærk: Dette er en del af serien "Komponerende software" (nu en bog!) Om indlæring af funktionel programmering og kompositionssoftwareteknikker i JavaScriptES6 + fra bunden af. Bliv hængende. Der kommer meget mere af dette!
Køb bogen | Indeks |

For dem, der ikke kender JavaScript eller ES6 +, er dette beregnet som en kort introduktion. Uanset om du er en nybegynder eller en erfaren JavaScript-udvikler, lærer du måske noget nyt. Det følgende er kun beregnet til at ridse overfladen og få dig ophidset. Hvis du vil vide mere, skal du bare udforske dybere. Der er meget mere forude.

Den bedste måde at lære at kode er at kode. Jeg anbefaler, at du følger med ved hjælp af et interaktivt JavaScript-programmeringsmiljø som CodePen eller Babel REPL.

Alternativt kan du slippe af med at bruge Node- eller browserkonsolens REPL'er.

Udtryk og værdier

Et udtryk er et stykke kode, der evalueres til en værdi.

Følgende er alle gyldige udtryk i JavaScript:

7;
7 + 1; // 8
7 * 2; // 14
'Hej'; // Hej

Værdien af ​​et udtryk kan gives et navn. Når du gør det, evalueres udtrykket først, og den resulterende værdi tildeles navnet. Til dette bruger vi const-nøgleordet. Det er ikke den eneste måde, men det er den, du bruger mest, så vi holder os med const i øjeblikket:

const hello = 'Hej';
Hej; // Hej

var, lad og konst

JavaScript understøtter to flere variable deklarationssøgeord: var og let. Jeg kan godt lide at tænke på dem med hensyn til rækkefølgen af ​​udvælgelse. Som standard vælger jeg den strengeste erklæring: const. En variabel, der er erklæret med const-nøgleordet, kan ikke tildeles igen. Den endelige værdi skal tildeles på erklæringstidspunktet. Dette lyder muligvis stift, men begrænsningen er en god ting. Det er et signal, der fortæller dig, "den værdi, der er tildelt dette navn, ændres ikke". Det hjælper dig med fuldt ud at forstå, hvad navnet betyder med det samme, uden at du behøver at læse hele funktionen eller blokere rækkevidden.

Nogle gange er det nyttigt at tildele variabler. For eksempel, hvis du bruger manuel, imperativ iteration snarere end en mere funktionel tilgang, kan du iterere en tæller, der er tildelt med let.

Fordi var fortæller dig mindst om variablen, er det det svageste signal. Siden jeg begyndte at bruge ES6, har jeg aldrig med vilje erklæret en var i et reelt softwareprojekt.

Vær opmærksom på, at når en variabel er erklæret med let eller const, vil ethvert forsøg på at erklære den igen resultere i en fejl. Hvis du foretrækker mere eksperimentel fleksibilitet i REPL (Read, Eval, Print Loop) miljøet, kan du muligvis bruge var i stedet for const til at erklære variabler. Omklassificering var er tilladt.

Denne tekst bruger const for at få dig i vanen med at misligholde const for faktiske programmer, men du er velkommen til at erstatte var med henblik på interaktiv eksperimentering.

typer

Indtil videre har vi set to typer: tal og strenge. JavaScript har også booleaner (sandt eller falskt), arrays, objekter med mere. Vi kommer til andre typer senere.

En matrix er en ordnet liste over værdier. Tænk på det som en kasse, der kan indeholde mange genstande. Her er den array bogstavelige notation:

[1, 2, 3];

Det er selvfølgelig et udtryk, der kan få et navn:

const arr = [1, 2, 3];

Et objekt i JavaScript er en samling nøgle: værdipar. Det har også en bogstavelig notation:

{
  nøgle: 'værdi'
}

Og selvfølgelig kan du tildele et objekt til et navn:

const foo = {
  bar: 'bar'
}

Hvis du vil tildele eksisterende variabler til objektegenskaber med samme navn, er der en genvej til det. Du kan bare indtaste variabelnavnet i stedet for at give både en nøgle og en værdi:

const a = 'a';
const oldA = {a: a}; // lang, overflødig måde
const oA = {a}; // kort en sød!

Bare for sjov, lad os gøre det igen:

const b = 'b';
const oB = {b};

Objekter kan let sammensættes til nye objekter:

const c = {... oA, ... oB}; // {a: 'a', b: 'b'}

Disse prikker er operatøren med objekter. Det itererer over egenskaberne i oA og tildeler dem til det nye objekt, gør det samme for oB, tilsidesætter alle taster, der allerede findes på det nye objekt. Fra denne skrivning er objektspredning en ny, eksperimentel funktion, der muligvis ikke er tilgængelig i alle de populære browsere endnu, men hvis det ikke fungerer for dig, er der en erstatning: Object.assign ():

const d = Object.assign ({}, oA, oB); // {a: 'a', b: 'b'}

Kun lidt mere at skrive i eksempelet Object.assign (), og hvis du komponerer masser af objekter, kan det endda gemme dig noget at skrive. Bemærk, at når du bruger Object.assign (), skal du passere et destinationsobjekt som den første parameter. Det er det objekt, som egenskaber kopieres til. Hvis du glemmer og udelader destinationsobjektet, vil det objekt, du sender i det første argument, blive muteret.

Efter min erfaring er det normalt en fejl at mutere et eksisterende objekt i stedet for at oprette et nyt objekt. I det mindste er det tilbøjelig til fejl. Vær forsigtig med Object.assign ().

nedbrydningen

Både objekter og arrays understøtter destruktion, hvilket betyder, at du kan udtrække værdier fra dem og tildele dem til navngivne variabler:

const [t, u] = ['a', 'b'];
t; // 'a'
u; // 'b'
const blep = {
  blop: 'blop'
};

// Følgende svarer til:
// const blop = blep.blop;
const {blop} = klap;
blop; // 'blop'

Som med arrayeksemplet ovenfor, kan du ødelægge til flere opgaver på én gang. Her er en linje, som du kan se i mange Redux-projekter:

const {type, nyttelast} = handling;

Sådan bruges det i forbindelse med en reducer (meget mere om dette emne kommer senere):

const myReducer = (tilstand = {}, handling = {}) => {
  const {type, nyttelast} = handling;
  switch (type) {
    sag 'FOO': returner Object.assign ({}, tilstand, nyttelast);
    standard: returneringstilstand;
  }
};

Hvis du ikke ønsker at bruge et andet navn til den nye binding, kan du tildele et nyt navn:

const {blop: bloop} = klap;
bloop; // 'blop'

Læs: Tildel blep.blop som bloop.

Sammenligninger og ternaries

Du kan sammenligne værdier med den strenge ligestillingsoperatør (nogle gange kaldet “tredobbelt ligestilling”):

3 + 1 === 4; // rigtigt

Der er også en sjusket ligestillingsoperatør. Det er formelt kendt som ”ligestillet” operatør. Uformelt er "dobbelt lig". Double equals har en gyldig use-case eller to, men det er næsten altid bedre at standard til === operatoren i stedet.

Andre sammenligningsoperatører inkluderer:

  • > Større end
  • > = Større end eller lig med
  • <= Mindre end eller lig med
  • ! = Ikke ens
  • ! == Ikke streng lige
  • && Logisk og
  • || Logisk eller

Et ternært udtryk er et udtryk, der giver dig mulighed for at stille et spørgsmål ved hjælp af en komparator og evaluere til et andet svar afhængigt af, om udtrykket er sandt eller ej:

14 - 7 === 7? 'Jep!' : 'Nix.'; // Jepp!

Funktioner

JavaScript har funktionsudtryk, som kan tildeles navne:

const dobbelt = x => x * 2;

Dette betyder det samme som den matematiske funktion f (x) = 2x. Tales højt, den funktion læser f af x er lig med 2x. Denne funktion er kun interessant, når du anvender den til en bestemt værdi på x. For at bruge funktionen i andre ligninger, skriver du f (2), der har samme betydning som 4.

Med andre ord, f (2) = 4. Du kan tænke på en matematikfunktion som en kortlægning fra input til output. f (x) er i dette tilfælde en kortlægning af inputværdier for x til tilsvarende outputværdier lig med produktet af inputværdien og 2.

I JavaScript er værdien af ​​et funktionsudtryk selve funktionen:

dobbelt; // [Funktion: dobbelt]

Du kan se funktionsdefinitionen ved hjælp af .toString () -metoden:

double.toString (); // 'x => x * 2'

Hvis du vil anvende en funktion på nogle argumenter, skal du påkalde den med et funktionsopkald. Et funktionsopkald anvender en funktion til dets argumenter og evalueres til en returværdi.

Du kan påkalde en funktion vha. (argument1, argument2, ... hvile). For at påkalde vores dobbeltfunktion skal du bare tilføje parenteserne og give en værdi til at fordoble:

dobbelt (2); // 4

I modsætning til nogle funktionelle sprog er disse parenteser meningsfulde. Uden dem kaldes funktionen ikke:

dobbelt 4; // SyntaxError: Uventet nummer

Underskrifter

Funktioner har underskrifter, der består af:

  1. Et valgfrit funktionsnavn.
  2. En liste over parametertyper i parentes. Parametrene kan eventuelt navngives.
  3. Type returværdien.

Type underskrifter behøver ikke at være specificeret i JavaScript. JavaScript-motoren finder ud af, hvilke typer der er ved kørsel. Hvis du giver tilstrækkelig ledetråde, kan underskriften også udledes af udviklerværktøjer som IDE'er (Integrated Development Environment) og Tern.js ved hjælp af dataflowanalyse.

JavaScript mangler sin egen notationsnotationsfunktion, så der er et par konkurrerende standarder: JSDoc har historisk været meget populær, men det er akavet ordret, og ingen generer at holde doc-kommentarerne ajour med koden, så mange JS-udviklere har stoppede med at bruge det.

TypeScript og Flow er i øjeblikket de store konkurrenter. Jeg er ikke sikker på, hvordan jeg udtrykker alt, hvad jeg har brug for i nogen af ​​dem, så jeg bruger Rtype kun til dokumentation. Nogle mennesker falder tilbage på Haskells karri-kun Hindley – Milner-typer. Jeg ville meget gerne se et godt notationssystem standardiseret til JavaScript, hvis det kun er til dokumentationsformål, men jeg tror ikke, at nogen af ​​de nuværende løsninger er op til opgaven i øjeblikket. For øjeblikket kan du skubbe og gøre dit bedste for at følge med på de underlige typer underskrifter, der sandsynligvis ser lidt anderledes ud end hvad du bruger.

functionName (param1: Type, param2: Type) => Type

Signaturen til dobbelt er:

dobbelt (x: n) => Antal

På trods af det faktum, at JavaScript ikke kræver underskrifter for at blive annoteret, vil det stadig være vigtigt at vide, hvad underskrifter er, og hvad de betyder for at kommunikere effektivt om, hvordan funktioner bruges, og hvordan funktioner er sammensat. De fleste genanvendelige hjælpeprogrammer til funktionskomposition kræver, at du videregiver funktioner, der deler den samme typesignatur.

Standardparameterværdier

JavaScript understøtter standardparameterværdier. Følgende funktion fungerer som en identitetsfunktion (en funktion, der returnerer den samme værdi, som du videregiver), medmindre du kalder den med udefineret eller simpelthen ikke overfører noget argument overhovedet - så returnerer den nul i stedet:

const orZero = (n = 0) => n;

For at indstille en standard skal du blot tildele den til parameteren med = operatoren i funktionssignaturen, som i n = 0, ovenfor. Når du tildeler standardværdier på denne måde, kan indtastningsværktøjer, som f.eks. Tern.js, Flow eller TypeScript, udlede typesignaturen på din funktion automatisk, selvom du ikke eksplicit erklærer typeanmærkninger.

Resultatet er, at med de rigtige plugins installeret i din editor eller IDE, vil du være i stand til at se funktionssignaturer vist inline, mens du skriver funktionskald. Du vil også være i stand til at forstå, hvordan du bruger en funktion på et øjeblik baseret på dens kaldesignatur. Brug af standardopgaver uanset hvor det er fornuftigt kan hjælpe dig med at skrive mere selvdokumenterende kode.

Bemærk: Parametre med standardindstillinger tæller ikke med til funktionens .længdeegenskab, som vil kaste værktøjer, såsom autokurry, der afhænger af værdien .længde, af. Nogle karryværktøjer (såsom lodash / curry) giver dig mulighed for at videregive en brugerdefineret arity til at omgå denne begrænsning, hvis du støder på det.

Navngivne argumenter

JavaScript-funktioner kan tage objektlitterære som argumenter og bruge destruktiv tildeling i parametersignaturen for at opnå ækvivalenten med navngivne argumenter. Bemærk, du kan også tildele standardværdier til parametre ved hjælp af standardparameterfunktionen:

const createUser = ({
  name = 'Anonym',
  avatarThumbnail = '/avatars/anonymous.png'
}) => ({
  navn,
  avatarThumbnail
});
const george = createUser ({
  navn: 'George',
  avatarThumbnail: 'avatars / shades-emoji.png'
});
george;
/ *
{
  navn: 'George',
  avatarThumbnail: 'avatars / shades-emoji.png'
}
* /

Hvil og spred

Et fælles træk ved funktioner i JavaScript er muligheden for at samle en gruppe af resterende argumenter i funktionssignaturen ved hjælp af resten-operatøren: ...

For eksempel kaster den følgende funktion simpelthen det første argument og returnerer resten som en matrix:

const aTail = (hoved, ... hale) => hale;
aTail (1, 2, 3); // [2, 3]

Hvil samler individuelle elementer sammen til en matrix. Spredning gør det modsatte: det spreder elementerne fra en matrix til individuelle elementer. Overvej dette:

const shiftToLast = (hoved, ... hale) => [... hale, hoved];
shiftToLast (1, 2, 3); // [2, 3, 1]

Arrays i JavaScript har en iterator, der påberåbes, når spredningsoperatøren bruges. For hvert element i matrixen leverer iteratoren en værdi. I udtrykket [... hale, hoved] kopierer iteratoren hvert element i rækkefølge fra halearrayet ind i det nye array oprettet af den omgivende bogstavelige notation. Da hovedet allerede er et individuelt element, plupper vi det bare på enden af ​​matrixen, og vi er færdige.

beredningsmetode

En curried-funktion er en funktion, der tager flere parametre ad gangen: Den tager en parameter og returnerer en funktion, der tager den næste parameter osv. Indtil alle parametre er leveret, på hvilket tidspunkt applikationen er afsluttet og den endelige værdi returneres.

Curry og delvis applikation kan aktiveres ved at returnere en anden funktion:

const highpass = cutoff => n => n> = cutoff;
const gt4 = highpass (4); // highpass () returnerer en ny funktion

Du behøver ikke at bruge pilefunktioner. JavaScript har også et funktionsnøgleord. Vi bruger pilefunktioner, fordi funktionens nøgleord er meget mere at skrive. Dette svarer til definitionen highPass () ovenfor:

const highpass = funktion highpass (cutoff) {
  returfunktion (n) {
    return n> = cutoff;
  };
};

Pilen i JavaScript betyder omtrent “funktion”. Der er nogle vigtige forskelle i funktionsadfærd afhængigt af hvilken type funktion du bruger (=> mangler sin egen dette og kan ikke bruges som konstruktør), men vi kommer til disse forskelle, når vi kommer dertil. For nu, når du ser x => x, skal du tænke "en funktion, der tager x og returnerer x". Så du kan læse const highpass = cutoff => n => n> = cutoff; som:

“Highpass er en funktion, der tager cutoff og returnerer en funktion, der tager n og returnerer resultatet af n> = cutoff”.

Da highpass () returnerer en funktion, kan du bruge den til at oprette en mere specialiseret funktion:

const gt4 = highpass (4);
GT4 (6); // rigtigt
GT4 (3); // falsk

Autokurry lader dig curryfunktioner automatisk for maksimal fleksibilitet. Sig, at du har en funktion add3 ():

const add3 = curry ((a, b, c) => a + b + c);

Med autokurry kan du bruge det på flere forskellige måder, og det vil returnere det rigtige, afhængigt af hvor mange argumenter du sender:

add3 (1, 2, 3); // 6
add3 (1, 2) (3); // 6
add3 (1) (2, 3); // 6
ADD3 (1) (2) (3); // 6

Beklager Haskell fans, JavaScript mangler en indbygget autokurry-mekanisme, men du kan importere en fra Lodash:

$ npm installation - gem lodash

Derefter i dine moduler:

import curry fra 'lodash / curry';

Eller du kan bruge følgende trylleformular:

// Lille, rekursiv autokurry
const curry = (
  f, arr = []
) => (... args) => (
  a => a.length === f.length?
    f (... a):
    karri (f, a)
) ([... arr, ... args]);

Funktionskomposition

Selvfølgelig kan du komponere funktioner. Funktionssammensætning er processen med at overføre returneringsværdien for en funktion som et argument til en anden funktion. I matematisk notation:

f. g

Hvilket oversætter til dette i JavaScript:

f (g (x))

Det evalueres indefra og ud:

  1. x evalueres
  2. g () anvendes til x
  3. f () anvendes til returværdien af ​​g (x)

For eksempel:

const inc = n => n + 1;
inc (dobbelt (2)); // 5

Værdien 2 overføres til dobbelt (), som frembringer 4. 4 overføres til inc (), som evalueres til 5.

Du kan videregive ethvert udtryk som et argument til en funktion. Udtrykket evalueres, før funktionen anvendes:

inc (dobbelt (2) * dobbelt (2)); // 17

Da dobbelt (2) evaluerer til 4, kan du læse det som inc (4 * 4), som evaluerer til inc (16), som derefter evalueres til 17.

Funktionssammensætning er central for funktionel programmering. Vi får meget mere om det senere.

Arrays

Arrays har nogle indbyggede metoder. En metode er en funktion, der er knyttet til et objekt: normalt en egenskab for det tilknyttede objekt:

const arr = [1, 2, 3];
arr.map (double); // [2, 4, 6]

I dette tilfælde er arr objektet, .map () er en egenskab for objektet med en funktion til en værdi. Når du påberåber den, bliver funktionen anvendt på argumenterne samt en speciel parameter kaldet denne, som automatisk indstilles, når metoden påberopes. Denne værdi er, hvordan .map () får adgang til indholdet i matrixen.

Bemærk, at vi overfører dobbeltfunktionen som en værdi på kortet i stedet for at kalde den. Det skyldes, at kort tager en funktion som et argument og anvender det på hvert element i matrixen. Det returnerer en ny matrix, der indeholder de værdier, der er returneret med dobbelt ().

Bemærk, at den oprindelige arr-værdi er uændret:

arr; // [1, 2, 3]

Metodekæde

Du kan også kæde metodekald. Metodekæde er processen med direkte at kalde en metode på en funktions returværdi uden at skulle henvise til returværdien med navn:

const arr = [1, 2, 3];
arr.map (dobbelt) .map (double); // [4, 8, 12]

Et predikat er en funktion, der returnerer en boolesk værdi (sand eller falsk). Metoden .filter () tager et predikat og returnerer en ny liste ved kun at vælge de elementer, der passerer predikatet (return true), der skal inkluderes i den nye liste:

[2, 4, 6] .filter (gt4); // [4, 6]

Ofte vil du vælge emner fra en liste og derefter kortlægge disse elementer til en ny liste:

[2, 4, 6] .filter (gt4). Kort (dobbelt); [8, 12]

Bemærk: Senere i denne tekst ser du en mere effektiv måde at vælge og kortlægge på samme tid ved hjælp af noget, der kaldes en transducer, men der er andre ting, du først skal udforske.

Konklusion

Hvis dit hoved roterer lige nu, skal du ikke bekymre dig. Vi har næsten ikke ridset overfladen på en masse ting, som fortjener meget mere efterforskning og overvejelse. Vi kommer snart tilbage og udforsker nogle af disse emner meget mere dybde.

Køb bogen | Indeks |

Lær mere på EricElliottJS.com

Videotimer med interaktive kodeudfordringer er tilgængelige for medlemmer af EricElliottJS.com. Hvis du ikke er medlem, tilmeld dig i dag.

Eric Elliott er en distribueret systemekspert og forfatter af bøgerne, "Komponerende software" og "Programmering af JavaScript-applikationer". Som medstifter af DevAnywhere.io lærer han udviklere de færdigheder, de har brug for til at arbejde eksternt og omfatte balance mellem arbejde og liv. Han bygger og rådgiver udviklingshold til kryptoprojekter og har bidraget til softwareoplevelser for Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC og topindspilningskunstnere, herunder Usher, Frank Ocean, Metallica og mange flere.

Han nyder en fjern livsstil med den smukkeste kvinde i verden.