Beginner 4
Forberedelse
Vi har før set tupler. Dette er en datastruktur der kan holde et bestemt antal elementer. Vi kan udtrække et element fra en tuple ved pattern match eller ved brug at elem
funktionen:
elem({0, 1, 2, 3}, 2)
Bemærk: elem
er ikke en anonym funktion og derfor skal vi ikke bruge et punktum når vi kalder den.
Lister
Tuples er gode til at holde et kendt antal forskellige værdier. Men ofte ønsker vi at den kode vi skriver kan arbejde med et vilkårligt antal værdier. Det bruger vi en liste til:
l = [0, 1, 2, 3, 4]
Vi kan bruge pattern matching til at navngive (med variable) listens hovede (dét første element, på engelsk head) og listens hale (de resterende elementer, på engelsk tail). Dette vil vise sig at være meget brugbart når vi senere skal gennemløbe en liste og gøre noget med hvert element. Pattern matching mod en liste ser således ud (og kan godt kombineres med eksempelvis pattern matching af en tupel):
[h|t] = l
{h, t}
I den sidste celle kontruerer vi en tupel hvor det første element er værdien af variablen h
og det sidste element er værdien af variablen t
.
Vi kan også benytte syntaksen fra højre side af et pattern match til at ptte et nyt hovede på en liste:
[-1|l]
Vi har også mulighed for at bruge at
funktionen i Enum
modulet til at indeksere ind i l
. Her skal det bemærkes at det første element er element $0$. Det ser således ud:
Enum.at(l, 3)
Højereordensfunktioner
Med Elixir kommer en række meget anvendelige højereordensfunktioner. Vi skal i dag se på tre af dem fra Enum
modulet:
-
map
Udfører en funktion på samtlige elementer i en liste og resulterer en tilsvarende liste af returværdier. -
filter
Konstruerer en liste af alle de elementer fra en input liste hvor et udtryk (i form af en funktion) evaluerer til sand. -
reduce
Gennemløber en liste og udfører en funktion på en initiel værdi og listens første værdi. Derefter returværdien af dette funktionskald og den næste værdi. Derefter returværdien af dette funktionskald og den næste værdi. Og så videre … indtil vi er nået til den sidste returværdi, som returneres.
Men lad os se lidt eksempler:
Først definerer vi en liste med en række heltallige værdier:
l1 = [12, -3, -14, -2, 1, 6, 7, -4, 16, 5, 3, -7, 0, 0, 2, -1, 8]
Nu kan vi bruge map
til at lægge 1 til samtlige elementer af listen:
l2 = Enum.map(l1, fn element -> element + 1 end)
Derefter kan vi fjerne alle elementer der ikke er negative (≥ skrives som >=
):
l3 = Enum.filter(l2, fn element -> element >= 0 end)
Slutteligt kan vi udregne summen:
Enum.reduce(l3, 0, fn element, acc -> element + acc end)
Pipelining
Dette kode kan også skrives som:
Enum.reduce(
Enum.filter(
Enum.map(
[12, -3, -14, -2, 1, 6, 7, -4, 16, 5, 3, -7, 0, 0, 2, -1, 8],
fn element -> element + 1 end),
fn element -> element >= 0 end),
0,
fn element, acc -> element + acc end)
Der er helt utroligt hyppigt hyppigt at man ser sådan en kæde af funktionskald hvor output af én funktion bruges som input til den næste. For at gøre dette mønster lettere at læste har man indført noget syntaks for at pipe output fra én funktion ind som første element i den næste. Dette gøres ved hjælp af |>
operatoren (som bliver tegnes som wen højrepil). Det ser således ud:
[12, -3, -14, -2, 1, 6, 7, -4, 16, 5, 3, -7, 0, 0, 2, -1, 8]
|> Enum.map(fn element -> element + 1 end)
|> Enum.filter(fn element -> element >= 0 end)
|> Enum.reduce(0, fn element, acc -> element + acc end)
Moduler med Funktioner
Men hvordan er disse højereordensfunktioner lavet?
Navngivne funktioner kan placeres i moduler. I næste celle ses et modul der implementerer map
funktionen. Teknisk set er der to funktionserklæringer, men Elixir benytter pattern matching til at afgøre hvilken erklæring der skal kaldes. Hvis den første erklæring matcher, så er det denne der bliver kaldt. Hvis ikke checkes den næste.
I dette tilfælde håndterer den første erklæring den tomme streng, og den anden håndtere alle tilfælde hvor input ikke er en tom streng. Den anden erklæring bliver ved med at klippe hovedet af strengen af indtil den er tom.
Implementationen udnytter at en funktions input værdier kan være funktioner til at sende en funktion med rundt. Denne kaldes til at transformere hvert hovede til et nyt hovede der nærmest sys ind i en ny liste som løbende konstrueres. Det fungere lidt som en lynlås.
defmodule MyEnum do
def map([], _fun), do: []
def map([h|t], fun) do
[fun.(h) | map(t, fun)]
end
end
Og den virker præcist som den map
funktion der er defineret i Enum
:
[12, -3, -14, -2, 1, 6, 7, -4, 16, 5, 3, -7, 0, 0, 2, -1, 8]
|> MyEnum.map(fn element -> element + 1 end)
Øvelse
Lav i følgende celle en kopi af indholdet af cellen med definitionen af MyEnum
. Tilføj og implementer en udgave af filter
funktionen ved siden af den allerede eksisterende map
funktion:
Næste trin …
Når du er færdig går du til næste øvelse her.