Intro to Records in Elm

Free functions, and other useful bits

Records | Image credit Pexels Unsplash

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
}
Free with your records

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


Similar records | Image credit

#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 -1
newId : 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.

Fields in Ireland | Image credit

#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 the as 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:

Show your support

Clapping shows how much you appreciated Wouter In t Velt’s story.