Funktionel programmering til Android-udviklere - Del 1

På det seneste har jeg brugt meget tid på at lære Elixir, et fantastisk funktionelt programmeringssprog, der er venligt for begyndere.

Dette fik mig til at tænke: hvorfor ikke bruge nogle af begreberne og teknikkerne fra den funktionelle verden i Android-programmering?

Når de fleste mennesker hører udtrykket Funktionel programmering, tænker de på Hacker News-indlæg, der jammer om Monads, Higher Order-funktioner og abstrakte datatyper. Det ser ud til at være et mystisk univers langt fjernet fra den daglige programmørs arbejde, kun forbeholdt de mægtigste hackere, der stammer fra Númenors rige.

Nå, skru det ! Jeg er her for at fortælle dig, at du også kan lære det. Du kan også bruge det. Du kan også oprette smukke apps med det. Apps, der har elegante, læsbare kodebaser og har færre fejl.

Velkommen til Functional Programming (FP) til Android-udviklere. I denne serie lærer vi fundamentet i FP, og hvordan vi kan bruge dem i god gammel Java og nye fantastiske Kotlin. Ideen er at holde begreberne baseret på praktisk og undgå så meget akademisk jargon som muligt.

FP er et stort emne. Vi lærer kun de begreber og teknikker, der er nyttige til at skrive Android-kode. Vi besøger muligvis et par begreber, som vi ikke kan bruge direkte for fuldstændighedens skyld, men jeg vil prøve at holde materialet så relevant som muligt.

Parat? Lad os gå.

Hvad er funktionel programmering, og hvorfor skal jeg bruge den?

Godt spørgsmål. Udtrykket funktionel programmering er en paraply for en række programmeringskoncepter, som monikeren ikke helt gør retfærdighed over for. I sin kerne er det en stil med programmering, der behandler programmer som evaluering af matematiske funktioner og undgår mutabel tilstand og bivirkninger (vi taler snart om disse).

Kernen understreger FP:

  • Deklarativ kode - Programmører skal bekymre sig om hvad og lade compileren og runtime bekymre sig om hvordan .
  • Eksplicititet - Kode skal være så indlysende som muligt. Især bivirkningerskal isoleres for at undgå overraskelser. Datastrøm og fejlhåndtering defineres eksplicit og konstruktioner som GOTO- udsagn og undtagelser undgås, da de kan sætte din applikation i uventede tilstande.
  • Samtidighed - De fleste funktionelle koder er som standard samtidig på grund af et koncept kendt som funktionel renhed . Den generelle enighed ser ud til at være, at især denne egenskab får funktionel programmering til at stige i popularitet, da CPU-kerner ikke bliver hurtigere hvert år som før (se Moores lov), og vi er nødt til at gøre vores programmer mere samtidige for at drage fordel af af multi-core arkitekturer.
  • Højere ordensfunktioner - Funktioner er førsteklasses medlemmer ligesom alle de andre sproglige primitiver. Du kan sende funktioner rundt ligesom du ville have en streng eller en int.
  • Uforanderlighed - Variabler skal ikke ændres, når de initialiseres. Når en ting er skabt, er det den ting for evigt. Hvis du vil have det til at ændre sig, opretter du en ny ting. Dette er et andet aspekt af eksplicitet og undgåelse af bivirkninger. Hvis du ved, at en ting ikke kan ændre sig, har du meget mere tillid til dens tilstand, når du bruger den.

Erklærende, eksplicit og samtidige kode, der er lettere at begrunde og er designet til at undgå overraskelser? Jeg håber, jeg har vakt din interesse.

I denne første del af serien, lad os starte med nogle af de mest grundlæggende begreber i FP: Renhed , bivirkninger og ordning .

Rene funktioner

En funktion er ren, hvis dens output kun afhænger af dens input og ikke har nogen bivirkninger (vi taler om bivirkningsbit lige efter dette). Lad os se et eksempel, skal vi?

Overvej denne enkle funktion, der tilføjer to tal. Det læser et nummer fra en fil, og det andet nummer sendes ind som en parameter.

Java

int add(int x) { int y = readNumFromFile(); return x + y;}

Kotlin

fun add(x: Int): Int { val y: Int = readNumFromFile() return x + y}

Denne funktions output afhænger ikke kun af dens input. Afhængigt af hvad readNumFromFile () returnerer, kan den have forskellige udgange til den samme værdi af x . Denne funktion siges at være uren .

Lad os konvertere det til en ren funktion.

Java

int add(int x, int y) { return x + y;}

Kotlin

fun add(x: Int, y: Int): Int { return x + y}

Nu afhænger funktionens output kun af dens indgange. For et givet x og y returnerer funktionen altid den samme output. Denne funktion siges nu at være ren . Matematiske funktioner fungerer også på samme måde. En matematisk funktions output afhænger kun af dens indgange - Derfor er funktionel programmering meget tættere på matematik end den sædvanlige programmeringsstil, vi er vant til.

PS Et tomt input er stadig et input. Hvis en funktion ikke tager nogen input og returnerer den samme konstant hver gang, er den stadig ren.

PPS Egenskaben ved altid at returnere den samme output for en given input kaldes også referentiel gennemsigtighed, og du kan muligvis se den bruges, når du taler om rene funktioner.

Bivirkninger

Lad os undersøge dette koncept med det samme eksempel på tilføjelsesfunktion. Vi ændrer tilføjelsesfunktionen for også at skrive resultatet til en fil.

Java

int add(int x, int y) { int result = x + y; writeResultToFile(result); return result;}

Kotlin

fun add(x: Int, y: Int): Int { val result = x + y writeResultToFile(result) return result}

Denne funktion skriver nu resultatet af beregningen til en fil. dvs. det ændrer nu omverdenens tilstand. Denne funktion siges nu at have en bivirkning og er ikke længere en ren funktion.

Enhver kode, der ændrer omverdenens tilstand - ændrer en variabel, skriver til en fil, skriver til en DB, sletter noget osv. - siges at have en bivirkning.

Funktioner, der har bivirkninger, undgås i FP, fordi de ikke længere er rene og afhænger af historisk sammenhæng . Kodens kontekst er ikke selvstændig. Dette gør dem meget sværere at tænke over.

Lad os sige, at du skriver et stykke kode, der afhænger af en cache. Nu afhænger output af din kode af, om nogen skrev til cachen, hvad der var skrevet i den, hvornår den blev skrevet, om dataene er gyldige osv. Du kan ikke forstå, hvad dit program laver, medmindre du forstår alle de mulige tilstande af cachen afhænger det af. Hvis du udvider dette til at omfatte alle de andre ting, som din app afhænger af - netværk, database, filer, brugerinput og så videre, bliver det meget svært at vide, hvad der præcist foregår, og at passe det hele ind i dit hoved på én gang.

Betyder det, at vi ikke bruger netværk, databaser og cacher da? Selvfølgelig ikke. I slutningen af ​​udførelsen vil du have, at appen skal gøre noget. I tilfælde af Android-apps betyder det normalt at opdatere brugergrænsefladen, så brugeren rent faktisk kan få noget nyttigt fra vores app.

FPs største idé er ikke helt at give afkald på bivirkninger, men at indeholde og isolere dem. I stedet for at have vores app fyldt med funktioner, der har bivirkninger, skubber vi bivirkninger til kanterne på vores system, så de har så lidt indflydelse som muligt, hvilket gør vores app lettere at ræsonnere om. Vi taler om dette detaljeret, når vi udforsker en funktionel arkitektur til vores apps senere i serien.

Bestilling

Hvis vi har en masse rene funktioner, der ikke har nogen bivirkninger, bliver rækkefølgen, i hvilken de udføres, irrelevant.

Lad os sige, at vi har en funktion, der kalder 3 rene funktioner internt:

Java

void doThings() { doThing1(); doThing2(); doThing3();}

Kotlin

fun doThings() { doThing1() doThing2() doThing3()}

Vi ved med sikkerhed, at disse funktioner ikke afhænger af hinanden (da output fra en ikke er input fra en anden), og vi ved også, at de ikke vil ændre noget i systemet (da de er rene). Dette gør rækkefølgen, i hvilken de udføres, fuldstændig udskiftelige.

Ordren til udførelse kan blandes om og optimeres til uafhængige rene funktioner. Bemærk, at hvis input af doThing2 () var resultatet af doThing1 (), ville disse skulle udføres i rækkefølge, men doThing3 () kunne stadig beordres til at udføres før doThing1 ().

Hvad får denne bestillende ejendom os dog? Samtidighed, det er hvad! Vi kan køre disse funktioner på 3 separate CPU-kerner uden at bekymre os om at skrue noget op!

I mange tilfælde kan kompilatorer på avancerede rene funktionelle sprog som Haskell fortælle ved formelt at analysere din kode, om den er sammenfaldende eller ej, og kan forhindre dig i at skyde dig selv i foden med blokeringer, race betingelser og lignende. Disse compilere kan teoretisk også auto-parallelisere din kode (dette findes faktisk ikke i nogen compiler, jeg kender i øjeblikket, men forskning er i gang).

Selvom din kompilator ikke ser på disse ting, som programmør, er det dejligt at kunne fortælle, om din kode er samtidig bare ved at kigge på funktionsunderskrifterne og undgå grimme trådbugs, der forsøger at parallelisere tvingende kode, som måske er fuld af skjult bivirkninger.

Resumé

Jeg håber, at denne første del har fascineret dig om FP. Rene, bivirkningsfrie funktioner gør det meget nemmere at begrunde koden og er det første skridt til at opnå samtidighed.

Før vi kommer til samtidighed, skal vi dog lære om uforanderlighed . Vi gør netop det i del 2 af denne serie og ser, hvordan rene funktioner og uforanderlighed kan hjælpe os med at skrive enkel og let at forstå samtidig kode uden at ty til låse og mutexes.

Læs næste

Funktionel programmering til Android-udviklere - Del 2

Hvis du ikke har læst del 1, skal du læse den her: medium.com

Hvis du kunne lide dette, skal du klikke på? under. Jeg bemærker hver enkelt, og jeg er taknemmelig for hver enkelt af dem.

For mere overvejelser om programmering, følg mig, så du får besked, når jeg skriver nye indlæg.