Fortrolighed Bias holder dig tilbage: Det er tid til at omfavne pilefunktioner

“Anker” - Actor212 - (CC BY-NC-ND 2.0)

Jeg underviser i JavaScript for at leve. For nylig har jeg blandet mig rundt i min læseplan for at undervise curry-pilfunktioner før - inden for de første lektioner. Jeg flyttede det tidligere i læseplanen, fordi det er en ekstremt værdifuld færdighed, og studerende henter currying med pile meget hurtigere, end jeg troede, de ville.

Hvis de kan forstå det og drage fordel af det tidligere, hvorfor ikke lære det tidligere?

Bemærk: Mine kurser er ikke designet til folk, der aldrig har rørt ved en kodelinie før. De fleste studerende tilslutter sig efter at have brugt mindst et par måneder på kodning - alene, i en bootcamp eller professionelt. Jeg har dog set mange juniorudviklere med ringe eller ingen erfaring, der hurtigt optager disse emner.

Jeg har set en flok studerende få en fungerende fortrolighed med curry-pilfunktioner inden for en enkelt 1-timers lektion. (Hvis du er medlem af "Lær JavaScript med Eric Elliott", kan du se lektionen på ES6 Curry & Composition på 55 minutter lige nu).

Når jeg ser hvor hurtigt eleverne samler det op og begynder at udøve deres nyfundne curry-kræfter, er jeg altid en smule overrasket, når jeg lægger curry-pilfunktioner på Twitter, og Twitterverse reagerer med forargelse ved tanken om at påføre den "ulæselige" kode på de mennesker, der bliver nødt til at vedligeholde det.

Lad mig først give dig et eksempel på, hvad vi taler om. Første gang jeg bemærkede tilbageslag var Twitter-svaret til denne funktion:

const secret = msg => () => msg;

Jeg blev chokeret, da folk på Twitter beskyldte mig for at forsøge at forvirre folk. Jeg skrev den funktion for at demonstrere, hvor let det er at udtrykke curry funktioner i ES6. Det er den enkleste praktiske anvendelse og udtryk for en lukning, som jeg kan tænke på i JavaScript. (Relateret: “Hvad er en lukning?”).

Det svarer til følgende funktionsudtryk:

const secret = funktion (msg) {
  returfunktion () {
    returner msg;
  };
};

secret () er en funktion, der tager en msg og returnerer en ny funktion, der returnerer msg. Det drager fordel af lukninger for at fastsætte værdien af ​​msg til den værdi, du giver i hemmelighed ().

Sådan bruger du det:

const mySecret = hemmelig ('hej');
Min hemmelighed(); // 'Hej'

Det viser sig, at "dobbeltpil" er det, der forvirrede mennesker. Jeg er overbevist om, at dette er en kendsgerning:

Med fortrolighed er in-line-pilfunktioner den mest læsbare måde at udtrykke curried-funktioner i JavaScript.

Mange mennesker har argumenteret for mig, at den længere form er lettere at læse end den kortere form. De er til dels rigtigt, men for det meste forkert. Det er mere ordlyst og mere eksplicit, men ikke lettere at læse - i det mindste ikke for nogen, der er fortrolig med pilefunktioner.

De indvendinger, jeg så på Twitter, gik bare ikke sammen med den glatte læringsoplevelse, som mine studerende nød. Efter min erfaring tager eleverne curry-pilfunktioner som fisk tager vand. Inden for dage efter at have lært dem er de en med pilene. De slynger dem ubesværet for at tackle alle mulige kodningsudfordringer.

Jeg ser ikke noget tegn på, at pilefunktioner er “svære” for dem at lære, læse eller forstå - når de først har foretaget den indledende investering i at lære dem i løbet af et par 1-timers lektioner og undersøgelser.

De læser let curry-pilfunktioner, som de aldrig har set før, og forklarer mig, hvad der foregår. De skriver naturligvis deres egne, når jeg byder på en udfordring for dem.

Med andre ord, så snart de bliver fortrolige med at se curry-pilfunktioner, har de ingen problemer med dem. De læser dem så let som du læser denne sætning - og deres forståelse afspejles i meget enklere kode med færre fejl.

Hvorfor nogle mennesker synes, at udtryk fra ældre funktioner ser "lettere" at læse

Fortrolighedsbias er en målbar menneskelig kognitiv bias, der fører os til at tage selvdestruktive beslutninger på trods af at vi er opmærksomme på en bedre mulighed. Vi bruger stadig de samme gamle mønstre på trods af at vi ved om bedre mønstre ud fra komfort og vane.

Du kan lære meget mere om kendskabsskævhed (og en masse andre måder, vi narrer os selv) fra den fremragende bog, "The Undoing Project: A Friendship that Changed Our Minds". Denne bog skal kræves læsning for enhver softwareudvikler, fordi den opfordrer dig til at tænke mere kritisk og teste dine antagelser for at undgå at falde i forskellige kognitive fælder - og historien om, hvordan disse kognitive fælder blev opdaget, er også virkelig god .

Ældre funktionsudtryk forårsager sandsynligvis fejl i din kode

I dag omskrev jeg en curried pilefunktion fra ES6 til ES5, så jeg kunne offentliggøre den som et open source-modul, som folk kunne bruge i gamle browsere uden at transpilere. ES5-versionen chokerede mig.

ES6-versionen var enkel, kort og elegant - kun 4 linjer.

Jeg troede helt sikkert, dette var den funktion, der kunne bevise for Twitter, at pilefunktionerne er overlegne, og at folk skulle opgive deres arvsfunktioner som den dårlige vane, de er.

Så jeg tweetede:

Her er teksten til funktionerne, hvis billedet ikke fungerer for dig:

// curry med pile
const composeMixins = (... mixins) => (
  instans = {},
  mix = (... fns) => x => fns.reduce ((acc, fn) => fn (acc), x)
) => mix (... mixins) (instans);
// vs ES5-stil
var composeMixins = funktion () {
  var mixins = [] .slice.call (argumenter);
  returfunktion (forekomst, mix) {
    hvis (! instans) instans = {};
    hvis (! mix) {
      mix = funktion () {
        var fns = [] .slice.call (argumenter);
        returfunktion (x) {
          return fns.reduce (funktion (acc, fn) {
            return fn (acc);
          }, x);
        };
      };
    }
    return mix.apply (null, mixins) (instans);
  };
};

Den pågældende funktion er en simpel indpakning omkring røret (), et standard funktionelt programmeringsværktøj, der ofte bruges til at komponere funktioner. En pipe () -funktion findes i lodash som lodash / flow, i Ramda som R.pipe (), og har endda sin egen operator på flere funktionelle programmeringssprog.

Det skal være for alle kendt med funktionel programmering. Som bør dens primære afhængighed: Reducer.

I dette tilfælde bruges det til at komponere funktionelle mixins, men det er en irrelevant detalje (og et helt andet blogindlæg). Her er de vigtige detaljer:

Funktionen tager ethvert antal funktionelle mixins og returnerer en funktion, der anvender dem den ene efter den anden i en rørledning - som en samlebånd. Hver funktionel mixin tager forekomsten som et input og klæber nogle ting på det, før det overføres til den næste funktion i rørledningen.

Hvis du udelader forekomsten, oprettes et nyt objekt til dig.

Nogle gange vil vi måske komponere mixins forskelligt. For eksempel kan det være en god idé at videregive compose () i stedet for pipe () for at vende prioritetsrækkefølgen.

Hvis du ikke behøver at tilpasse opførselen, skal du bare lade standard være i fred og få standard pipe () -adfærd.

Bare fakta

Meninger om læsbarhed til side, her er de objektive kendsgerninger, der vedrører dette eksempel:

  • Jeg har flere års erfaring med både ES5 og ES6 funktionsudtryk, pile eller andet. Fortrolighed bias er ikke en variabel i disse data.
  • Jeg skrev ES6-versionen på få sekunder. Den indeholdt nul bugs (som jeg er opmærksom på - den klarer alle dens enhedstest).
  • Det tog mig flere minutter at skrive ES5-versionen. I det mindste en størrelsesorden mere tid. Minutter kontra sekunder. Jeg mistede min plads i funktionens indrykk to gange. Jeg skrev 3 bugs, som alle var nødt til at fejlsøge og ordne. To af dem var jeg nødt til at ty til console.log () for at finde ud af, hvad der foregik.
  • ES6-versionen er 4 kodelinjer.
  • ES5-versionen er 21 linjer lang (17 indeholder faktisk kode).
  • På trods af sin kedelige ordlighed mister ES5-versionen faktisk noget af den informationssikkerhed, der er tilgængelig i ES6-versionen. Det er meget længere, men kommunikerer mindre, læs videre for detaljer.
  • ES6-versionen indeholder 2 spreads til funktionsparametre. ES5-versionen udelader spredningerne og bruger i stedet det implicitte argumenter-objekt, der skader læsbarheden af ​​funktionssignaturen (fidelity downgrade 1).
  • ES6-versionen definerer standard for mix i funktionssignaturen, så du tydeligt kan se, at det er en værdi for en parameter. ES5-versionen skjuler den detalje og skjuler den i stedet dybt inde i funktionskroppen. (troskab nedjustering 2).
  • ES6-versionen har kun 2 indrykkningsniveauer, hvilket hjælper med at afklare strukturen for, hvordan den skal læses. ES5-versionen har 6, og indlejringsniveauerne er uklare i stedet for at hjælpe læsbarheden af ​​funktionens struktur (fidelity downgrade 3).

I ES5-versionen optager pipe () det meste af funktionskroppen - så meget, at det er lidt vanvittigt at definere det inline. Den skal virkelig uddeles i en separat funktion for at gøre ES5-versionen læsbar:

var pipe = funktion () {
  var fns = [] .slice.call (argumenter);
  returfunktion (x) {
    return fns.reduce (funktion (acc, fn) {
      return fn (acc);
    }, x);
  };
};
var composeMixins = funktion () {
  var mixins = [] .slice.call (argumenter);
  returfunktion (forekomst, mix) {
    hvis (! instans) instans = {};
    hvis (! mix) mix = pipe;
    return mix.apply (null, mixins) (instans);
  };
};

Dette synes klart mere læseligt og forståeligt for mig.

Lad os se, hvad der sker, når vi anvender den samme læsbarhed “optimering” på ES6-versionen:

const pipe = (... fns) => x => fns.reduce ((acc, fn) => fn (acc), x);
const composeMixins = (... mixins) => (
  instans = {},
  mix = rør
) => mix (... mixins) (instans);

Ligesom ES5-optimeringen er denne version mere ordbog (den tilføjer en ny variabel, der ikke var der før). I modsætning til ES5-versionen er denne version ikke væsentligt mere læsbar efter at have abstraheret definitionen af ​​rør. Når alt kommer til alt havde den allerede et variabelt navn, der klart er tildelt det i funktionssignaturen: mix.

Definitionen af ​​blanding var allerede indeholdt på sin egen linje, hvilket gør det usandsynligt for læserne at blive forvirrede over hvor det slutter, og resten af ​​funktionen fortsætter.

Nu har vi 2 variabler, der repræsenterer den samme ting i stedet for 1. Har vi opnået meget? Ikke åbenlyst, nej.

Så hvorfor er ES5-versionen åbenlyst bedre med den samme funktion abstraheret?

Fordi ES5-versionen naturligvis er mere kompleks. Kilden til denne kompleksitet er kernen i denne sag. Jeg hævder, at kilden til kompleksiteten koger ned til syntaksstøj, og at syntaksstøj skjuver betydningen af ​​funktionen og ikke hjælper.

Lad os skifte gear og fjerne nogle flere variabler. Lad os bruge ES6 til begge eksempler og sammenligne kun pilefunktioner kontra ældre funktionsudtryk:

var composeMixins = funktion (... mixins) {
  returfunktion (
    instans = {},
    mix = funktion (... fns) {
      returfunktion (x) {
        return fns.reduce (funktion (acc, fn) {
          return fn (acc);
        }, x);
      };
    }
  ) {
    return mix (... mixins) (instans);
  };
};

Dette ser markant mere læsbar ud for mig. Alt, hvad vi har ændret, er, at vi drager fordel af syntaks til hvile og standardparameter. Naturligvis skal du være fortrolig med hvile og standardsyntax for at denne version skal være mere læsbar, men selvom du ikke er det, synes jeg det er indlysende, at denne version stadig er mindre rodet.

Det hjalp meget, men det er stadig klart for mig, at denne version stadig er rodet nok til at abstraktion af rør () til sin egen funktion åbenlyst ville hjælpe:

const pipe = funktion (... fns) {
  returfunktion (x) {
    return fns.reduce (funktion (acc, fn) {
      return fn (acc);
    }, x);
  };
};
// Ældre funktionsudtryk
const composeMixins = funktion (... mixins) {
  returfunktion (
    instans = {},
    mix = rør
  ) {
    return mix (... mixins) (instans);
  };
};

Det er bedre, ikke? Nu hvor mix-tildelingen kun optager en enkelt linje, er strukturens funktion meget mere klar - men der er stadig for meget syntaksstøj til min smag. I composeMixins () er det ikke for mig et øjeblik, hvor en funktion slutter og en anden begynder.

I stedet for at kalde funktionsorganer, ser det ud til, at det funktionelle nøgleord visuelt smelter sammen med identifikatorerne omkring det. Der er funktioner skjult i min funktion! Hvor slutter parametersignaturen, og funktionslegemet begynder? Jeg kan finde ud af det, hvis jeg ser nøje, men det er ikke visuelt indlysende for mig.

Hvad nu hvis vi kunne slippe af med funktionsnøgleordet og kalde returværdier ved visuelt at pege på dem med en stor fedtpil => i stedet for at skrive et returord, der passer sammen med de omkringliggende identifikatorer?

Det viser sig, vi kan, og her er hvordan det ser ud:

const composeMixins = (... mixins) => (
  instans = {},
  mix = rør
) => mix (... mixins) (instans);

Nu skal det være klart, hvad der foregår. composeMixins () er en funktion, der tager et hvilket som helst antal mixins og returnerer en funktion, der tager to valgfrie parametre, instans og mix. Det returnerer resultatet af rørforekomst gennem de sammensatte mixins.

Bare en ting til… hvis vi anvender den samme optimering på røret (), omdannes det magisk til en enforing:

const pipe = (... fns) => x => fns.reduce ((acc, fn) => fn (acc), x);

Med denne definition på en linje er fordelen ved at abstrahere den ud i sin egen funktion mindre klar. Husk, at denne funktion findes som et værktøj i Lodash, Ramda og en masse andre biblioteker, men er det virkelig værd at det er vigtigt at importere et andet bibliotek?

Er det endda værd at trække det ud i sin egen linje? Sandsynligvis. De er virkelig to forskellige funktioner, og at adskille dem gør det mere tydeligt.

På den anden side præciseres det, når man ser det online, forventninger til type og brug, når man ser på parametersignaturen. Her er hvad der sker, når vi opretter det online:

const composeMixins = (... mixins) => (
  instans = {},
  mix = (... fns) => x => fns.reduce ((acc, fn) => fn (acc), x)
) => mix (... mixins) (instans);

Nu er vi tilbage til den originale funktion. Undervejs kasserede vi ingen mening. Ved at erklære vores parametre og standardværdier inline tilføjede vi faktisk oplysninger om, hvordan funktionen bruges, og hvordan værdierne for parametrene kan se ud.

Al den ekstra kode i ES5-versionen var bare støj. Syntaksestøj. Det tjente ikke noget nyttigt formål undtagen at akklimatisere mennesker, der ikke kender curry-pilfunktioner.

Når du først har fået tilstrækkelig fortrolighed med curry-pilfunktioner, skal det være klart, at den originale version er mere læsbar, fordi der er meget mindre syntaks at gå tabt i.

Det er også mindre udsat for fejl, fordi der er meget mindre overfladeareal for fejl at gemme sig i.

Jeg formoder, at der er mange bugs, der gemmer sig i ældre funktioner, der ville blive fundet og fjernet, hvis du skulle opgradere til pilefunktioner.

Jeg formoder også, at dit team ville blive markant mere produktivt, hvis du lærte at omfavne og favorisere mere af den kortfattede syntaks, der er tilgængelig i ES6.

Selvom det er rigtigt, at nogle gange er tingene lettere at forstå, hvis de gøres eksplicit, er det også rigtigt, at mindre kode som en generel regel er bedre.

Hvis mindre kode kan udføre den samme ting og kommunikere mere uden at ofre nogen mening, er det objektivt bedre.

Nøglen til at kende forskellen er mening. Hvis flere kode undlader at tilføje mere betydning, bør den kode ikke findes. Dette koncept er så grundlæggende, det er en velkendt retningslinje for naturligt sprog.

Den samme stilretningslinje gælder for kildekode. Omfavn det, og din kode bliver bedre.

I slutningen af ​​dagen, et lys i mørket. Som svar på endnu en tweet, hvor ES6-versionen er mindre læselig:

Tid til at blive fortrolig med ES6, karry og funktionskomposition.

Næste skridt

“Lær JavaScript med Eric Elliott” -medlemmer kan se lektionen på ES6 Curry & Composition på 55 minutter lige nu.

Hvis du ikke er medlem, går du glip af det!

Eric Elliott er forfatteren af ​​“Programmering af JavaScript-applikationer” (O’Reilly) og “Lær JavaScript med Eric Elliott”. Han 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 tilbringer det meste af sin tid i San Francisco Bay-området med den smukkeste kvinde i verden.