Hvad betyder det, når kode er “let at ræsonnere om”?

Du har sikkert hørt udtrykket ”let at ræsonnere om” nok gange til, at dine ører bløder.

Første gang jeg hørte dette udtryk, anede jeg ikke, hvad personen mente med det.

Betyder det funktioner, der er lette at forstå?

Betyder det funktioner, der fungerer korrekt?

Betyder det funktioner, der er lette at analysere?

Efter et stykke tid havde jeg hørt "let at ræsonnere om" i så mange sammenhænge, ​​at jeg regnede med, at det bare var endnu et semi-meningsløst udvikler-buzzword.

... Men er det virkelig meningsløst?

Sandheden er, at udtrykket har en betydelig betydning. Det fanger en ret kompleks idé, hvilket gør afkodning det lidt vanskeligt. Trickiness til side, at have en høj forståelse af, hvordan "let at ræsonnere om" kode ser ud, hjælper os absolut med at skrive bedre programmer.

Til dette formål vil dette indlæg være dedikeret til at dissekere udtrykket "let at ræsonnere om", da det vedrører de tekniske samtaler, vi har som udviklere.

Forstå dit programs opførsel

Når du først har skrevet et stykke kode, vil du typisk også forstå programmets opførsel, hvordan det interagerer med andre dele af programmet og de egenskaber, det viser.

Tag for eksempel koden nedenfor. Dette skal multiplicere en række tal med 3.

Hvordan kan vi teste, at det fungerer efter hensigten? En logisk måde er at videregive en række arrays som input og sikre, at den altid returnerer arrayet med hvert element ganget med 3.

Ser godt ud indtil videre. Vi har testet, at funktionen gør, hvad vi vil have den til at gøre.

Men hvordan ved vi, at det ikke gør, hvad vi ikke vil have det? For eksempel med nøje inspektion kan vi se, at funktionen muterer det oprindelige array.

Er det det, vi har tænkt os? Hvad hvis vi har brug for referencer til både det originale array og det resulterende array? Alt for dårlig, tror jeg.

Lad os derefter se, hvad der sker, hvis vi passerer det samme array en række forskellige tidspunkter - returnerer det altid det samme resultat for en given input?

Åh åh. Det ser ud som når vi passerede array [1, 2, 3] til funktionen første gang, vendte det tilbage [3, 6, 9] , men senere vendte det tilbage [49, 98, 147] . Det er meget forskellige resultater.

Det er fordi funktionen multiplyByThree er afhængig af en ekstern variabel multiplikator . Så hvis den eksterne tilstand af programmet får den variable multiplikator til at skifte mellem opkald til funktionen multiplyByThree , ændres funktionsmåden, selvom vi sender det samme array til funktionen.

Eeek. Ser ikke så godt ud mere. Lad os grave lidt dybere.

Indtil videre har vi testet perfekte array-input. Hvad nu hvis vi skulle gøre dette:

Hvad I alverden?!?

Programmet så godt ud på overfladen - når vi tager et par minutter på at evaluere det, var det dog en anden historie.

Vi så, at det nogle gange returnerer en fejl, nogle gange returnerer det samme, som du har sendt til det, og kun lejlighedsvis returnerer det forventede resultat. Desuden har det nogle utilsigtede bivirkninger (mutering af det oprindelige array) og synes ikke at være konsistent i, hvad det returnerer for et givet input (da det er afhængigt af ekstern tilstand).

Lad os nu se en lidt anden multiplyByThree- funktion:

Ligesom ovenfor kan vi teste for rigtighed.

Ser godt ud indtil videre.

Lad os også teste for at se, om det gør, hvad vi ikke vil have det. Muterer det det oprindelige array?

Nix. Den oprindelige matrix er intakt!

Returnerer den samme output for en given input?

Jep! Da multiplikator variabel er nu inden for rammerne af den funktion, selv hvis vi erklærer en dublet multiplikator variabel i den globale rækkevidde, vil det ikke påvirke resultatet.

Returnerer det det samme, hvis vi sender en række forskellige typer argumenter?

Jep! Nu opfører funktionen sig mere forudsigeligt - den returnerer enten en fejl eller et nyt resulterende array.

På dette tidspunkt, hvor sikre er vi på, at denne funktion gør nøjagtigt, hvad vi vil have den? Har vi dækket alle kantsager? Lad os prøve et par mere:

For pokker. Det ser ud til, at vores funktion stadig har brug for lidt arbejde. Når matrixen i sig selv indeholder uventede emner, som udefineret eller strenge, ser vi underlig opførsel igen.

Lad os prøve at ordne det ved at tilføje endnu en check i vores for-loop-kontrol for ugyldige matrixelementer:

Med denne nye funktion, hvorfor ikke prøve disse to kanttilfælde igen:

Sød. Nu returnerer den også en fejl, hvis nogen af ​​elementerne i arrayet ikke er tal i stedet for noget tilfældigt funky output.

Endelig en definition

Ved at gennemgå ovenstående trin har vi langsomt opbygget en funktion, der er let at ræsonnere over, fordi den har disse nøglekvaliteter:

  1. Har ikke utilsigtede bivirkninger
  2. Stoler ikke på eller påvirker den eksterne tilstand
  3. Med det samme argument vil det altid returnere den samme tilsvarende output (også kendt som "referentiel gennemsigtighed").

Måder, vi kan garantere disse egenskaber

Der er mange forskellige måder, vi kan garantere, at vores kode er let at ræsonnere om. Lad os se på et par:

Enhedstest

For det første kan vi skrive enhedstest for at isolere kodestykker og kontrollere, at de fungerer som beregnet:

Enhedstest som disse hjælper os med at verificere, at vores kode fungerer korrekt, og giver os levende dokumentation om, hvordan små stykker af det samlede system fungerer. Advarslen med enhedstest er, at medmindre du er meget tankevækkende og grundig, er det utrolig let at gå glip af problematiske kanttilfælde.

For eksempel ville vi aldrig have fundet ud af, at den oprindelige matrix bliver muteret, medmindre vi på en eller anden måde tænkte at teste for det. Så vores kode er kun så robust som vores tests.

Typer

Ud over tests kan vi muligvis også bruge typer til at gøre det lettere at begrunde koden. For eksempel, hvis vi brugte en statisk checker til JavaScript som Flow, kunne vi sikre, at input-arrayet altid er et array med tal:

Typer tvinger os til eksplicit at angive, at input-arrayet er et array med tal. De hjælper med at skabe begrænsninger på vores kode, som forhindrer mange slags runtime-fejl, som vi så tidligere. I vores tilfælde behøver vi ikke længere tænke på at kontrollere for at sikre, at hver vare i matrixen er et tal - dette er en garanti, der gives os med typer.

Uforanderlighed

Endelig er en anden ting, vi kan gøre, at bruge uforanderlige data. Uforanderlige data betyder bare, at dataene ikke kan ændres, når de er oprettet. Dette hjælper med at undgå utilsigtede bivirkninger.

I vores tidligere eksempel, hvis input-arrayet var uforanderligt, ville det have forhindret den uforudsigelige opførsel, hvor den originale array blev muteret. Og hvis multiplikatoren var uforanderlig, ville det forhindre situationer, hvor en anden del af programmet kan mutere vores multiplikator.

Nogle af måderne vi kan høste fordelene ved uforanderlighed på er ved at bruge et funktionelt programmeringssprog, der iboende sikrer uforanderlighed eller ved at bruge et eksternt bibliotek som Immutable.js, der håndhæver uforanderlighed oven på et eksisterende sprog.

Som en sjov udforskning bruger jeg Elm, et indtastet funktionelt programmeringssprog, til at demonstrere, hvordan uforanderlighed hjælper os:

Dette lille uddrag gør det samme som vores JavaScript multiplyByThree- funktion fra før, bortset fra at det nu er i Elm. Da Elm er et indtastet sprog, kan du se på linje 6, at vi definerer input- og outputtyperne til funktionen multiplyByThree, da begge er en liste med tal. Funktionen selv anvender den grundlæggende kort operation for at generere den resulterende matrix.

Nu hvor vi har defineret vores funktion i Elm, lad os lave en sidste runde af de samme tests, vi gjorde for vores tidligere multiplyByThree- funktion:

Som du kan se, er resultatet, hvad vi forventede, og originalArray er ikke blevet muteret.

Lad os nu kaste Elm for et trick og prøve at mutere multiplikatoren:

Aha! Elm begrænser dig fra at gøre dette. Det kaster en meget venlig fejl.

Hvad hvis vi skulle sende en streng som et argument i stedet for en række numre?

Det ser ud til, at Elm også fangede det. Fordi vi erklærede argumentet som en liste over tal, kan vi ikke videregive andet end en liste over tal, selvom vi prøvede!

Vi snydte lidt i dette eksempel ved at bruge et funktionelt programmeringssprog, der har både typer og uforanderlighed. Det punkt, jeg ønskede at bevise, er, at med disse to funktioner behøver vi ikke længere tænke på manuelt at tilføje kontroller for alle kantsager for at få de tre egenskaber, vi diskuterede. Typer og uforanderlighed garanterer, at vi for os, og til gengæld, lettere kan ræsonnere om vores kode?

Nu er det din tur til at tænke over din kode

Jeg udfordrer dig til at tage et øjeblik, næste gang du hører nogen sige, "XYZ gør det let at begrundes med kode" eller "ABC gør er svært at begrunde om kode." Erstat det smarte buzzword med de egenskaber, der er nævnt ovenfor, og prøv at forstå, hvad personen mener. Hvilke egenskaber har kodestykket, der gør det let at begrunde?

Personligt har udøvelsen af ​​denne øvelse hjulpet mig kritisk til at tænke på kode og til gengæld motiveret mig til at tænke på, hvordan jeg skriver programmer, der er lettere at ræsonnere om. Jeg håber, det gør det samme for dig også!

Jeg ville elske at høre dine tanker om andre egenskaber, som jeg måske har savnet, som du synes er vigtige. Skriv din feedback i kommentarerne!