Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Beginner 4

src/livebook/beginner4.livemd

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.