Intro to Records in Elm
Free functions, and other useful bits
--
Records are everywhere in Elm. Your first Model in Elm (or maybe your second) was a record. We use them to hold data of various other types together in one structure. They are a bit like objects in javascript. You can define a record with multiple fields of different types, and then use them elsewhere, e.g. in a list.
type alias User =
{ id : Int
, name : String
, age : Int
}type alias UserList = List User
Records have limitations too. You cannot iterate or map
over the fields in a record. To the frequent frustration of those coming from javascript (like me), Elm has no record[key]
to dynamically access fields. In Elm (like in javascript), the fields in a record can be of any type (which is 👍). But dynamic typing would break the type safety guarantee of the Elm compiler.
Fortunately, there is plenty of other goodness in Elm Records. Here’s a couple of lesser known features of Records, that you may find useful.
#1. Free functions
Records come with free functions too. Let’s examine the following User
record:
type alias User =
{ id : String
, name : String
, age : Int
}
The basic features are that you can get the name of a user like this:
someName = someUser.name
And you can create an updated user with a new name and age like this:
newUser =
{ oldUser
| name = newName
, age = newAge
}
But there is more! With the User
record defined as a type alias, you also get the following functions for free:
User : String -> String -> Int -> User
.id : User -> String
.name : User -> String
.age : User -> Int
The capitalized function User
is a constructor. You can use it to construct a User
by supplying all fields as arguments. In a previous post, you can find more detail on using constructors.
The other free functions are getter functions. That’s right, these functions begin with a .
(dot).
Let’s look at a practical use case: Let’s say you have a List
of User
s, and you want to have only the names of these users. With the free dot-function, all you need is this:
userNames = List.map .name users
The List.map
function takes two arguments, the first is a function to turn some a
into some b
. In this case, it turns a User
into a String
(where the string contains the name). The second argument is a list of a
, in this case the list of users. The output is a list of b
. So with this simple statement, all the names are extracted from our list of user records. Using the free dotted function.
#2. Different records with the same fields
What if you have the same field that appears in multiple record types? In my own code, I frequently have fields likeid : Int
which appear in multiple record types. Something like this:
type alias Exam =
{ id : Int
, subject : String
}type alias StudyBlock =
{ id : Int
, time : Float
}type alias ExamItem =
{ id : Int
, title : String
, pages : Int
}
Whenever my code hands out a new id
to create some new record, I need to find out the highest id
in my data, and create a new one. And I like my ids to be unique across my app.
This is where I use the following pattern:
type alias RecordWithID a =
{ a | id : Int }maxId : List (RecordWithID a) -> Int
maxId list =
list
|> List.map (.id)
|> List.maximum
|> Maybe.withDefault -1newId : Data -> Int
newId data =
(maxId data.exams)
|> max (maxId data.examItems)
|> max (maxId data.studyBlocks)
|> (+) 1
The beauty of this is, the function maxId
can handle records with different fields. The function only cares about the used field id
. So you can call this function with a List of any type of record, as long as the record has an id
field.
#3. Efficient access to record fields in functions and patterns
A somewhat hidden feature of Elm is that you can selectively access fields in the records for use in functions. Which is particularly handy for nested records.
This is probably best explained in an example. Let’s say you have a record like this:
type alias MovingThing =
{ thing : { content : String }
, x : Int
, y : Int
}
What if you want a function that updates the content
field in the thing
in this record. One way to do this in Elm is like this:
setContent : String -> MovingThing -> MovingThing
setContent newContent movingThing =
let
oldThing = movingThing.thing
newThing = { oldThing | content = newContent }
in
{ movingThing | thing = newThing }
But you can also do this:
setContent : String -> MovingThing -> MovingThing
setContent newContent ({ thing } as movingThing) =
{ movingThing
| thing = { thing | content = newContent }
}
The argument ({ thing } as movingThing)
says:
- From the record, I want to be able to access the field
thing
, and I don’t care about whatever other fields are there. - And I want to be able to reference the complete record too as an argument named
movingThing
. That’s what theas
statement does.
This allows you to get to the nested thing
record inside the MovingThing
, without using the let
statement.
You can use variations of this pattern too. If you only want to access to a field, { y }
(without the as
) is sufficient. Or if you want access to multiple fields, you could also do e.g. { thing, x, y }
.
This works in case statements too. Suppose your MovingThing
is tracked in someSituation
state, which has the following type:
type Situation
= NoMovement
| Alive MovingThing
You can use the same trick:
assess situation =
case situation of
Alive ({ thing } as movingThing) ->
...
NoMovement ->
...
Further reading
Hopefully these 3 tricks will help you get more mileage out of your records in Elm, and make your Elm code more compact and readable.
For further reading, you could check out these resources:
- The official docs have the basic intro into records in Elm.
- The elm-sortable-table examples (by Evan himself 👍) also use the dotted-getter-functions to render a record in the table.