Quickstart Elm 0.19, part 9

Comparing Strings and Records in Elm 0.19

By: Ajdin Imsirovic 29 October 2019

In this article, we’ll compare Strings and Records in a couple of simple Elm apps. We will take two very similar apps and then compare code improvements between the two versions. Doing this will allow us to understand how we can improve our own Elm code.

A close-up of a race track with an Elm logo overlaid

Note: Examples in this article use Elm 0.19.

A simple app using a String to store the Model

Let’s start with a simple app that holds our app’s state in a String:

module Main exposing (main)

import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)


type alias Model =
    String


initialModel : Model
initialModel =
    "This is some text"


type Msg
    = Text


update : Msg -> Model -> Model
update msg model =
    case msg of
        Text ->
            model ++ "!"



view : Model -> Html Msg
view model =
    div []
    [ div [] [ text model ]
    , button [ onClick Text ] [ text "Add exclamation mark" ]
    ]


main : Program () Model Msg
main =
    Browser.sandbox
        { init = initialModel
        , view = view
        , update = update
        }

This app is available live on Ellie app.

A simple app using a Record to store the Model

Here’s the exact same app, only this time we’re using a Record to store the Model.

module Main exposing (main)

import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)


type alias Model =
    { text : String }


initialModel : Model
initialModel =
    { text = "This is some text" }


type Msg
    = Text


update : Msg -> Model -> Model
update msg model =
    case msg of
        Text ->
            { model | text = model.text ++ "!" }



view : Model -> Html Msg
view model =
    div []
    [ div [] [ text model.text ]
    , button [ onClick Text ] [ text "Add exclamation mark" ]
    ]


main : Program () Model Msg
main =
    Browser.sandbox
        { init = initialModel
        , view = view
        , update = update
        }

This version of the app can also be found on Ellie app.

Let’s now compare the first version of the app and the second one:

module Main exposing (main)

import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)

{-
as a RECORD
type alias Model =
    { text : String }
-}
type alias Model =
    String

{-
as a RECORD
initialModel : Model
initialModel =
    { text = "This is some text" }
-}
initialModel : Model
initialModel =
    "This is some text"


type Msg
    = Text


update : Msg -> Model -> Model
update msg model =
    case msg of
        Text ->
            -- as a RECORD
            -- { model | text = model.text ++ "!" }
            model ++ "!"



view : Model -> Html Msg
view model =
    div []
    {-
    -- as a RECORD
    [ div [] [ text model.text ]
    -}
    [ div [] [ text model ]
    , button [ onClick Text ] [ text "Add exclamation mark" ]
    ]


main : Program () Model Msg
main =
    Browser.sandbox
        { init = initialModel
        , view = view
        , update = update
        }

As you can see, above we’re using the humble String as our Model. In each place in the code above we’re adding a comment so that we can compare the way a Record would be used instead of this String.

Why is it better to use a Record than a String to model our data?

What’s the problem with the String-as-a-model app? Or, why is the code in the Record-as-a-model app better?

Because Records are more versatile.

We can add more stuff to a Record and then use it if we need to. Or choose not to use it and just leave it there — no harm, no foul.

Anyway, let’s prove that by updating our Record-based app.

We’re adding another member to our Record. This one will track the number of clicks. We’re calling it entered.

This is the update in the model type alias:

type alias Model =
    { text : String
    , entered : Int
    }

We also need to update the initialModel:

initialModel : Model
initialModel =
    { text = "This is some text" 
    , entered = 0
    }

Next, we need to instruct the update function how to deal with the new shape of our Record:

update : Msg -> Model -> Model
update msg model =
    case msg of
        Text ->
            -- as an EXTENDED RECORD
            { model | text = model.text ++ "!", entered = model.entered + 1 }

Finally, we’ll update the view with another div function; it is there to just statically display the changes to the model.entered member of our Record:

-- code skipped for brevity
, div [] [ text (Debug.toString model.entered) ]
-- code skipped for brevity

Here’s the fully updated app:

module Main exposing (main)

import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)


-- as an EXTENDED RECORD
type alias Model =
    { text : String
    , entered : Int
    }


-- as an EXTENDED RECORD
initialModel : Model
initialModel =
    { text = "This is some text" 
    , entered = 0
    }


type Msg
    = Text


update : Msg -> Model -> Model
update msg model =
    case msg of
        Text ->
            -- as an EXTENDED RECORD
            { model | text = model.text ++ "!", entered = model.entered + 1 }


view : Model -> Html Msg
view model =
    div []
    
    -- as an EXTENDED RECORD
    [ div [] [ text model.text ]
    , button [ onClick Text ] [ text "Add exclamation mark" ]
    , div [] [ text (Debug.toString model.entered) ]
    ]


main : Program () Model Msg
main =
    Browser.sandbox
        { init = initialModel
        , view = view
        , update = update
        }

Here’s our improved app live online.

And here is the embed of the above live app:

Let’s quickly go over these improvements:

In the previous app we had only a String inside a Record. In the newly updated app we are extending that Record with another label, entered. This label holds a value of type Int.

We set the value of Model.entered to zero.

In the update function, we pattern match for msg using a case-of expression, and the Text pattern. However, the way the model gets updated is different between the previous app and this updated one. Initially, we were just updating the model by setting the text to the new value. In the newly updated app, we also increment the value of the model.entered field, by adding 1 to it each time the Text pattern is matched.

Finally, in the newly updated app we extend the view function by adding another div function that renders the text node with the current value of model.entered. Of course, since model.entered is an Int, we first need to convert it to String, with the help of the Debug.toString function.

Next, let’s improve our view function, using the String.concat API.

Improving the output of click-tracking div

Let’s think about how we can further improve the output in the second div tag in our view function. Instead of just showing the number of times a visitor has clicked the button, we could show a better message, something like: Button clicked X times.

The solution is simple. We’ll use the String.concat API, which takes in a List of Strings, and concatenates them together. I suggest you check out the official docs for String.concat before you look at the example below.

Here’s the one-line update to our second div function inside the view function:

, div [] [ text (String.concat ["Button clicked ", (Debug.toString model.entered), " times"] ) ]

Let’s now look at how this improvement works.

We’re taking three strings:

  • "Button clicked "
  • (Debug.toString model.enetered)
  • " times"

We’re then running the String.concat function on a List of these 3 strings.

We’re finally running the text function on the value that gets returned from running the String.concat function.

Finally, let’s look at writing this code a bit nicer, using the piping syntax:

, div [] 
        [ text
            <| String.concat [ "Button clicked "
                                , (Debug.toString model.entered)
                                , " times"
                                ]
        ]

We can also pipe the above function call the opposite way. This makes it feel more natural to read:

, div [] 
        [ String.concat [ "Button clicked "
                        , (Debug.toString model.entered)
                        , " times"
                        ]
        |> text
        ]

Conclusion

In this post we’ve looked at how using Records in Elm, we can model the data in our apps a lot better than just using primitive values such as Strings or numbers.

We also discussed how we can enrich our models by extending our Records with additional fields.

In the next article, we’ll have some more practice in Elm 0.19.

Feel free to check out my work here: