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.
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.