Introduktion til Elm

Funktionel programmering

Vores første Elm-hjemmeside

Så skal vi bygge hjemmesider!

Vi kan ikke skrive en hjemmeside ved at skrive i elm repl. elm repl er ikke rigtig lavet til at skrive kode, der fylder mere end en linje ad gangen - men det gør hjemmesider: Avancerede hjemmesider kan sagtens fylde 10.000 linjer kode! Derfor har vi har brug for at kunne skrive mange linjer efter hinanden i et tekstdokument. Her kommer Sublime Text ind i billedet igen.

Det første eksempel

Enhver Elm-hjemmeside følger en bestemt struktur. Den vil vi forklare nu, og vi skal også se et eksempel på en Elm-hjemmeside.

Forestil jer en meget simpel hjemmeside: Der er en knap og et tal. På knappen står der "Forøg". Tallet starter med at være 0. Når man trykker på "Forøg"-knappen, så vokser tallet med 1. Det kunne se for eksempel sådan her ud:

Output

Ovenstående hjemmeside er dynamisk, fordi tallet på siden ændrer sig når vi interagerer med siden (det vil sige: når vi trykker på knappen). Tænk på det, der foregår sådan her: Hjemmesiden starter med at se ud på en bestemt måde: Der er en knap, som ikke ændrer sig, og så er der et tal, som starter med at være 0. Tallet kan ændre sig. Når vi trykker på knappen, sender knappen en besked til tallet om at vokse med 1. Derefter bliver browserens vindue opdateret, og så bliver det nye tal vist til os.

Nu dykker vi ned i koden bag ovenstående eksempel. Vi skal forstå præcis, hvad der sker, og hvorfor det virker.

Elm-kode

Lad os springe ud i det og se på den Elm-kode, som genererer ovenstående hjemmeside.

Elm
module NumberGame exposing (main) import Browser import Html exposing (Html) import Html.Attributes import Html.Events -- MODEL type alias Model = Int init : Model init = 0 -- UPDATE type Msg = AddOne update : Msg -> Model -> Model update message model = case message of AddOne -> (model + 1) -- VIEW view : Model -> Html Msg view model = Html.div [] [ Html.div [] [ Html.h3 [] [ Html.text (String.fromInt model) ] ] , Html.div [] [ Html.button [ Html.Events.onClick AddOne ] [ Html.text "Forøg" ] ] ]

Wow, det føles måske som meget kode for så simpel en hjemmeside, men det er faktisk ikke så slemt, når man lige får fod på det! Lad os prøve at gå igennem koden - bid for bid. Koden er lige nu delt op i tre dele, adskilt af linjer, som starter med dobbelt bindestreg:

Når en linje starter med dobbelt bindestreg i Elm er det en kommentar til programmøren, og Elm ved derfor at koden skal springes over; så de ting der står efter dobbelt bindestreg er altså kun til os, der skriver koden, så vi kan holde overblikket.

Vi gennemgår nu delene!

Før -- MODEL
Det der står over -- MODEL kommer vi ikke til at røre ved. Det er en beskrivelse af hvilke moduler vi skal hente for at få adgang til de funktioner, vi skal bruge i vores kode. Som eksempel får vi brug for nogle HTML-agtige funktioner.
-- MODEL

Næste afsnit, som står mellem -- MODEL og -- UPDATE er hjemmesidens hjerte. Her definerer vi formen på den dynamiske data, som skal vises på hjemmesiden. I eksemplet ovenfor er den dynamiske data det tal som bliver vist. Vi kan altså repræsentere hele hjemmesidens dynamiske indhold med ét tal! Afsnittet her forklarer, hvordan vi gør det.

Afsnittet -- MODEL består af tre linjer:

-- MODEL

type alias Model = Int

init : Model
init = 0

Første linje begynder med type alias, som er den måde hvorpå vi kan lave forkortelser for allerede eksisterende typer. Lige nu er vores Model egentlig bare et tal af typen Int, men vi omdøber typen til Model for at understrege, at det er hjemmesidens data-model. Når senere skal de vise sig, når vi skal bygge avancerede hjemmesider, vil vores Model ofte være en record som består af mange felter.

Det sidste vi gør er at specificere en startværdi til modellen. Startværdien kalder vi init, som står for "initialize". Vi skriver at den skal have typen Model, og den skal have værdien 0 når hjemmesiden åbnes.

-- VIEW

Lad os lige hoppe ned til det sidste afsnit efter -- VIEW.

-- VIEW

view : Model -> Html Msg
view model =
    Html.div []
        [ Html.div [] 
            [ Html.h3 [] 
                [ Html.text (String.fromInt model) ]
            ]
        , Html.div [] 
            [ Html.button [ Html.Events.onClick AddOne ] 
                [ Html.text "Forøg" ] 
            ]
        ]

Her sker der en hel masse!

Afsnittet er bygget op omkring funktionen view, som har til opgave at vise hjemmesiden i browseren. Lad os se nærmere på view:

view : Model -> Html Msg

View tager en værdi af typen Model som input. Når hjemmesiden åbnes er den konkrete værdi, det bliver puttet ind i view-funktionen den værdi vi har defineret med init. I vores eksempel er det altså tallet 0. Det giver rigtig god mening, at view skal have den nuværende model som input, fordi den skal netop vise dataen i modellen!

view-funktionens output er straks mere kompliceret! Outputtet har typen Html Msg, som vi ikke er støt på indtil nu. Det er en lidt svær sag, som det kan være lidt svært at få sit hoved rundt om! Du kan tænke på værdier af typen Html Msg som noget Html-kode, som browseren kan forstå, og det er netop pointen: Outputtet af view-funktionen skal være en Html Msg som afspejler den HTML vi gerne vil vise i browseren. Groft sagt skal vi altså "bare" skrive noget HTML-kode i vores view-funktion.

Nu er din invending nok, at det der står i view-funktionen ovenfor ikke ligner det HTML-kode, vi tidligere har skrevet! Det er helt rigtigt, men ved nærmere eftersyn er der en klar sammenhæng: Hvis vi gerne vil skrive et <div>-tag direkte i HTML, ville vi skrive:
 <div class = "highlighted"> Her er tekst. </div> 
I Elm skriver vi i stedet:
Html.div [Attributes.class = "highlighted"] [Html.text "Her er tekst."]
Logikken her er at Html.div faktisk er en funktion!
Html.div : List (Html Attribute) -> List (Html Msg) -> Html Msg
Html.div tager altså to lister som input: Den første liste består af alle de attributter, som vi vil knytte til div-elementet. Den anden liste består af alle de HTML-elementer, som skal være efterkommere af denne <div>.

På samme måde er Html.h3 og Html.button funktioner, der hver tager to lister som input:

Html.h3 : List (Html Attribute) -> List (Html Msg) -> Html Msg
Html.button : List (Html Attribute) -> List (Html Msg) -> Html Msg

Måske har du allerede luret den: for hvert eneste HTML-element er der en funktion i Elm, nemlig Html.TAGNAME, hvor TAGNAME står for navnet på det til HTML-elementet hørende HTML-tag.

Hvis man vil have normal tekst skal man bruge Html.text-funktionen. Denne funktion genererer ikke noget HTML, og derfor tager den heller ikke hverken attributter eller efterkommere. Html.text-funktionen genererer bare helt almindelig tekst, og tager derfor kun en String som input.
Html.text : String Html Msg

Øvelse

Den følgende -funktion genererer den efterfølgende HTML til browseren. Arbejd eksempelt igennem og vær sikker på, at du forstår, hvorfor det er præcis denne HTML, der bliver genereret.

Elm (view-funktion)
view : Model -> Html Msg view model = Html.div [] [ Html.h3 [] [ Html.text "Overskrift" ] , Html.p [] [Html.text "Her er en lang tekst."] ]
HTML (i browseren)
<div> <h3>Overksrift</h3> <p>Her er en lang tekst.</p> </div>

Vi mangler kun at forstå to små dele af view-funktionen nu. Pyha, næsten i mål!

Html.text kan kun modtage en String, så vi får brug for at lave vores Model om fra en Int til en String. Det gør funktionen String.fromInt, som er defineret sådan her:

String.fromInt : Int -> String

På knappen skal der stå Forøg, og det er Html.text "Forøg", der sørger for det. Men der skal også ske noget, når vi trykker på knappen! Det funktionen Html.Events.onClick, der sørger for det. Når der sker noget på en hjemmeside kaldes det for et event. Et eksepel på et event er, når nogen trykker på en knap med musen. Funktionen Html.Events.onClick sætter en attribut på et HTML-element, som angiver hvilken besked, der skal sendes, når nogen trykker på HTML-elementet.

Html.Events.onClick : Msg -> Html Attribute
Inputtet AddOne er den besked, som knappen skal sende, når der bliver trykket på det. Man hvad er AddOne egentlig? Vi har jo slet ikke kigget på beskeder endnu! Det leder os videre til det sidste afsnit af koden.

-- UPDATE

Dette afsnit består af to dele.

-- UPDATE

type Msg
    = AddOne

update : Msg -> Model -> Model
update message model =
    case message of
        AddOne ->
            (model + 1)

Den første del definerer typen Msg, som fortæller præcis hvilke beskeder, browseren må sende. Lige nu er der kun én besked, nemlig AddOne. På en avanceret hjemmeside kan der ske mange forskellige events, så vi skal kunne sende mange forskellige slags beskeder. Efterhånden som vi udbygger hjemmesiden med flere events, tilføjer vi også flere varianter til typen Msg.

Den anden del er selve update-funktionen. Funktionen tager to input: en besked af typen Msg og den nuværende model, der selvfølgelig har typen Model.

update : Msg -> Model -> Model

Når vi gør noget på hjemmesiden (f.eks. trykker på en knap), så bliver der sendt en besked af typen Msg til update-funktionen. Udover beskeden får update også den nuværende model som input. Ud fra beskeden skal update-funktionen nu opdatere den nuværende model, og dens output er en ny, opdateret model. Det er præcis det der sker her:

update : Msg -> Model -> Model
update message model =
    case message of
        AddOne ->
            (model + 1)

Læg mærke til, at vi har lavet en case ... of, selvom der på nuværende tidspunkt kun er én mulig besked. Det er fordi, at vi allerede har gjort klar til, at der snart kommer flere beskeder til!

Overblik

Elm-programmer kører altid efter følgende illustration:

Vi ved allerede hvad en browser er, og ovenfor har vi selv lavet update og view i diagramet. Men hvad er Elm Runtime? Elm Runtime er hjernen bag det hele: det er det, der får det hele til løbe rundt. Når vi åbner vores hjemmeside, så starter kredsløbet, og det forløber sådan her:

  1. Elm Runtime sender den model, vi har specificeret med init, til view-funktionen.
  2. view-funktionen tager imod den nuværende model og genererer en Html Msg og sender den tilbage til Elm Runtime.
  3. Elm Runtime fortolker den Html Msg, genererer en visualisering og sender HTML videre til Browseren (der står DOM på tegningen, men det er i virkeligheden bare HTML der bliver sendt til browseren). Browseren forstår HTML-kode og viser det på din skærm.
  4. Browseren venter på, at du gør noget: Du kan for eksempel være, at du trykker på en knap eller skriver noget i et felt. Når/hvis det sker, så sender browseren en besked til Elm Runtime.
  5. Elm Runtime sender nu beskeden videre til update-funktionen. Sammen med beskeden sender Elm Runtime den nuværende model, så update-funktionen både ved, hvad der er sket i browseren (beskeden) og hvordan hjemmesiden ser ud lige nu (modellen). update-funktionen outputter en ny model baseret på den besked den har fået. Denne nye model bliver sendt tilbage til Elm Runtime.
  6. Herefter sender Elm Runtime den nye model til view-funktionen, og så kører loopet!

Ovenstående forklaring er måske lidt abstrakt, så her følger en gennemgang med udgangspunkt i vores eksempel ovenfor.

  1. Elm Runtime sender modellen 0 til view-funktionen.
  2. view-funktionen tager 0 som input og genererer følgende Html Msg, som sendes tilbage til Elm Runtime:
    Html.div []
            [ Html.div [] 
                [ Html.h3 [] 
                    [ Html.text (String.fromInt model) ]
                ]
            , Html.div [] 
                [ Html.button [ Html.Events.onClick AddOne ] 
                    [ Html.text "Forøg" ] 
                ]
            ]
  3. Elm Runtime fortolker den Html Msg, genererer følgende HTML, som sendes til brwoseren:
    <div>
    	<div>
    		<h3>0</h3>
    	</div>
    	<div>
    		<button>Forøg</button>
    	</div>	
    </div>
  4. Browseren venter på, at du trykker på knappen. Når/hvis det sker, så sender browseren beskeden AddOne til Elm Runtime.
  5. Elm Runtime sender nu beskeden AddOne og den nuværende model, som er 0, videre til update-funktionen. update-funktionen outputter en ny model, som er 1. Denne nye model bliver sendt tilbage til Elm Runtime.
  6. Herefter sender Elm Runtime den nye model til view-funktionen, og så kører loopet!

Nu er du klar til selv at programmere i Elm! Giv den gas med øvelserne herunder, og når du er færdig med dem, så venter der en spændende udfordring i kapitel 6.

Vejledning
Vi har snydt hjemmefra. Vi har allerede programmeret en hjemmeside, der ser ud som beskrevet ovenfor. Følg denne guide for at åbne hjemmesiden.
Øvelse
Følg ovenstående vejledning og åbn filen NumberGame.elm i browseren.
Øvelse

Åbn filen NumberGame.elm i Sublime Text. Tjek at der står samme kode som i kodevinduet ovenfor. Vi vil gerne tilføje en ny knap til eksemplet, hvor der står Formindsk, og når vi trykker på den knap skal tælleren på hjemmesiden mindskes med 1.

  1. Start med at tilføje en ekstra variant til Msg, og kald den MinusOne.
  2. Tilføj en linje ekstra i update-funktionen, så tælleren falder med 1, når update modtager en MinusOne-message.
  3. Tilføj en ekstra linje i Html-div'en inde i view-funktionen, hvor du tilføjer en knap mere. Teksten på knappen skal være Formindsk, og når vi trykker på knappen, skal der sendes en MinusOne-message.

Øvelse

Tag udgangspunkt i koden fra øvelsen ovenover, hvor der er to knapper på hjemmesiden. Vi vil gerne tilføje en ny knap som skal nulstille tælleren, når man trykker på knappen.

  1. Start med at tilføje en ekstra variant af Msg, som hedder Reset.
  2. Tilføj en linje ekstra i update-funktionen, så tælleren sættes til 0, når update modtager en Reset-message.
  3. Tilføj en ekstra linje i Html-div'en inde i view-funktionen, hvor du tilføjer en knap mere. Teksten på knappen skal være Reset, og når vi trykker på knappen, skal der sendes en Reset-message.

Øvelse

Tilføj en knap mere som øger tælleren med 10.