Adventures in Elm

Introduction

2016 for me personal was the year the I came in serious contact with functional languages. I started looking at F#, applied functional concepts (partial application, immutability, pure functions and so on) in JavaScript, gave RxJS a shot and fell in love with React.

A logical next step was looking at Elm. I was first introduced to Elm at a Socrates Meetup a while ago by Thomas Coopman. To be honest I was a bit overwhelmed at the time and a bit skeptical afterwards.

I decided to give Elm another shot and learned a lot by following Elm for beginners. Now I feel ready to create small Elm applications.

The Christmas presents application

We have this family tradition that on Christmas Eve we give presents to each other among cousins. Each cousin or niece draws a name and gives a present to that person.
For a very long time the name drawing was done in a low tech fashion. But in more recent years I create a little application to automate the process.

The first iteration was me playing around with Owin/Katana and the second with React/Redux.
This year I decide to go with Elm.

I assume you know a thing or two about Elm as I'm not really explaining every single detail.
This blogpost is merely a walkthrough of a little Elm application I wrote.

Getting started

I used elm-webpack-starter to get started because I would also play around with Bulma.

I populated the Main.elm file with

And filled in all the pieces.

The model

The model part is very easy. There is a list of Person, a result dictionary and String property to contain the name of a new Person.

type alias Person =  
    { name : String, id : Int }


type alias Model =  
    { people : List Person, result : Dict Int Int, newPerson : String }


init : ( Model, Cmd Msg )  
init =  
    ( (Model [] Dict.empty ""), Cmd.none )

Actions

Very few messages but there is a bit of a twist in determining the result dictionary.

type Msg  
    = UpdateNewPerson String
    | AddPerson
    | RemovePerson Int
    | Generate
    | Result (List Int)

View

Very standard Elm stuff here as well.
I'm listing the code below to get the complete picture but notice that not much in going on.

view : Model -> Html Msg  
view model =  
    div []
        [ createForm model.newPerson
        , createPeopleTable model.people
        , createGenerateButton model.people
        , createResultTable model
        ]

createForm : String -> Html Msg  
createForm currentName =  
    Html.form [ class "entry box", onSubmit AddPerson ]
        [ h4 [ class "subtitle" ] [ text "Personen toevoegen" ]
        , p [ class "control has-addons" ]
            [ input [ class "input", type_ "text", placeholder "Vul een nieuwe naam in", onInput UpdateNewPerson, value currentName ] []
            , a [ class (formButtonClass currentName), onClick AddPerson ] [ text "Toevoegen" ]
            ]
        ]

formButtonClass : String -> String  
formButtonClass currentName =  
    if String.isEmpty currentName then
        "button is-info is-disabled"
    else
        "button is-info"

createPeopleTable : List Person -> Html Msg  
createPeopleTable people =  
    table [ class "table is-striped" ]
        [ thead []
            [ tr []
                [ th [] [ text "Naam" ]
                , th [ colspan 3 ] [ text "Verwijderen" ]
                ]
            ]
        , tbody [] (List.map createPeopleTableRow people)
        ]


createPeopleTableRow : Person -> Html Msg  
createPeopleTableRow person =  
    tr []
        [ td [] [ text person.name ]
        , td [ class "is-icon" ]
            [ div [ class "button is-danger is-small", onClick (RemovePerson person.id) ]
                [ i
                    [ class "fa fa-times" ]
                    []
                ]
            ]
        ]


createGenerateButton : List Person -> Html Msg  
createGenerateButton people =  
    let
        disabledClass =
            if List.length people <= 1 then
                "is-disabled"
            else
                ""
    in
        button [ class ("button is-primary is-large " ++ disabledClass), onClick Generate, id "generate" ] [ text "Genereer" ]


createResultTable : Model -> Html Msg  
createResultTable model =  
    let
        resultList =
            Dict.toList model.result

        createRow =
            createResultRow model
    in
        table [ class "table is-striped" ]
            [ thead []
                [ tr []
                    [ th [] [ text "Resultaat" ]
                    , th [] []
                    , th [] []
                    ]
                ]
            , tbody [] (resultList |> List.map createRow)
            ]


createResultRow : Model -> ( Int, Int ) -> Html Msg  
createResultRow model ( id, for ) =  
    let
        subject =
            getNameById model id

        target =
            getNameById model for
    in
        tr []
            [ td [] [ text subject ]
            , td [] [ text "koopt voor" ]
            , td [] [ text target ]
            ]


getNameById : Model -> Int -> String  
getNameById model id =  
    List.filter (\p -> p.id == id) model.people
        |> List.head
        |> Maybe.map (\p -> p.name)
        |> Maybe.withDefault "???"

I created some functions to split everything up, used some Bulma classes and Bob's your uncle

Update

This is the part where it got a bit tricky.

update : Msg -> Model -> ( Model, Cmd Msg )  
update msg model =  
    case msg of
        UpdateNewPerson newValue ->
            ( { model | newPerson = newValue }, Cmd.none )

        AddPerson ->
            addPerson model

        RemovePerson id ->
            removePerson model id

        Generate ->
            ( model, generate model )

        Result ids ->
            processResult model ids


removePerson : Model -> Int -> ( Model, Cmd Msg )  
removePerson model id =  
    let
        nextModel =
            { model | people = (List.filter (\p -> p.id /= id) model.people) }
    in
        if (List.length nextModel.people) > 1 then
            ( nextModel, generate nextModel )
        else
            ( { nextModel | result = Dict.empty }, Cmd.none )


addPerson : Model -> ( Model, Cmd Msg )  
addPerson model =  
    let
        nextId =
            List.map (\p -> p.id) model.people
                |> List.maximum
                |> Maybe.withDefault 0
                |> (+) 1

        nextPerson =
            Person model.newPerson nextId
    in
        ( { model | people = nextPerson :: model.people, newPerson = "" }, Cmd.none )


processResult : Model -> List Int -> ( Model, Cmd Msg )  
processResult model ids =  
    let
        personIds =
            List.map (\p -> p.id) model.people

        merged =
            List.map2 (,) personIds ids
    in
        ( { model | result = Dict.fromList merged }, Cmd.none )

UpdateNewPerson, AddPerson and RemovePerson are straightforward but I scratched my head to generate the actual result. I couldn't figure out how to get a randomized list of the person ids to populate the result dictionary.

Generating the result

Problem

To determine the result of who buys for who I would basically take the list of ids and shuffle it until no indexes are the same. But such a function would not be a pure one as the outcome could be different each time you call it.

Solution

When I doubt stick to what you know! So I decide to tackle the problem in JavaScript using Elm's port system.

port generationRequest : List Int -> Cmd msg

generate : Model -> Cmd Msg  
generate model =  
    generationRequest (List.map (\p -> p.id) model.people)

Note that generate is being called when a Generate message enters the update function by the onClick event. The generationRequest command return from the update function can be captured in the JavaScript world and will contain an array of ids when called.

// pull in desired CSS/SASS files
require( './styles/main.scss' );

// inject bundled Elm app into div#main
var Elm = require( '../elm/Main' );  
var app = Elm.Main.embed( document.getElementById( 'main' ) );

app.ports.generationRequest.subscribe(function(ids){  
    app.ports.generationResponse.send(createResponse(ids));
})

function shuffle(array) {  
    var currentIndex = array.length, temporaryValue, randomIndex ;

    // While there remain elements to shuffle...
    while (0 !== currentIndex) {

        // Pick a remaining element...
        randomIndex = Math.floor(Math.random() * currentIndex);
        currentIndex -= 1;

        // And swap it with the current element.
        temporaryValue = array[currentIndex];
        array[currentIndex] = array[randomIndex];
        array[randomIndex] = temporaryValue;
    }

    return array;
}

function createResponse(persons){  
    var result = shuffle(persons.slice(0));

    var duplicates = result.some(function(id, index){
       return id === persons[index];
    });

    return duplicates ? createResponse(persons) : result;
}

Inside the subscribe function I call the generationResponse port in Elm to return the result.
In order to capture the result I need a subscription in Elm.

port generationResponse : (List Int -> msg) -> Sub msg

subscriptions : Model -> Sub Msg  
subscriptions model =  
    generationResponse Result


main : Program Never Model Msg  
main =  
    program
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }

So the port gets called, the subscription message will find its way into the update function (into processResult) and the result can used to populate the dictionary.

processResult : Model -> List Int -> ( Model, Cmd Msg )  
processResult model ids =  
    let
        personIds =
            List.map (\p -> p.id) model.people

        merged =
            List.map2 (,) personIds ids
    in
        ( { model | result = Dict.fromList merged }, Cmd.none )

The Source

Is on my github or you can view the result on https://nojaf.github.io/christmas-elm/.

Remarks

  • Comparing to my React/Redux example I enjoyed Elm a lot more. Mainly because there is so much goodness just out of the box where with JavaScript you need to set it up yourselves. I'm talking about the frameworks here (Redux/ImmutableJS/React/DevTools), a lot of these solutions are included in the Elm language/architecture.
  • I didn't write any tests and I actually feel ok with this. The Elm compiler is so brilliant that I trust the output once it gets compiled.
  • Elm really deserves a lot more attention. Too many developers are hyped with Angular2/TypeScript, Vuejs, React/Flow and others. Nothing wrong with these great frameworks, I merely encourage everyone to try out Elm as it changed the way I look at the JavaScript landscape.

Final words

I hope you enjoyed this blogpost and it all makes sense. If you have any suggestions or questions please leave a comment.

Yours truly,
nojaf

Florian Verdonck
Florian Verdonck

Florian Verdonck is web & .NET developer. He is passionate about clean code and is eager to learn new technologies.