3.19

Record

Just like tuples, records can hold values of different types, but they are much more flexible than tuples.

Creating a Record

Records are created with curly brackets and their elements are separated by commas.

> { name = "Firefly", creator = "Joss Whedon", episodes = 14 }
{ name = "Firefly", creator = "Joss Whedon", episodes = 14 }

Unlike all data structures we have seen so far, records allow us to give names to the value contained in each element.

Let’s assign the record above to a constant and create a few more of these.

> firefly = { name = "Firefly", creator = "Joss Whedon", episodes = 14 }
{ name = "Firefly", creator = "Joss Whedon", episodes = 14 }

> fringe = { name = "Fringe", creator = "J. J. Abrams", episodes = 100 }
{ name = "Fringe", creator = "J. J. Abrams", episodes = 100 }

> vice = { name = "Vice", creator = "Shane Smith", episodes = 58 }
{ name = "Vice", creator = "Shane Smith", episodes = 58 }

We created three records each containing information about a popular TV show. Let’s put them all in a list.

> tvShows = [ firefly, fringe, vice ]
...

Here’s the output after some formatting to make it look nicer:

[
  { name = "Firefly",
    creator = "Joss Whedon",
    episodes = 14
  },
  { name = "Fringe",
    creator = "J. J. Abrams",
    episodes = 100
  },
  { name = "Vice",
    creator = "Shane Smith",
    episodes = 58
  }
]

Creating records with the literal syntax (i.e. curly brackets) can be tedious. Is there a better way? Let’s find out. First, let’s give the structure that underlies each of the three records we just created a name.

> type alias TVShow = { name : String, creator : String, episodes : Int }

type alias gives a name to an existing type. Using it we have given the underlying structure a name called TVShow. Now we can create records much more cleanly.

> firefly = TVShow "Firefly" "Joss Whedon" 14
{ name = "Firefly", creator = "Joss Whedon", episodes = 14 }

> fringe = TVShow "Fringe" "J. J. Abrams" 100
{ name = "Fringe", creator = "J. J. Abrams", episodes = 100 }

> vice = TVShow "Vice" "Shane Smith" 58
{ name = "Vice", creator = "Shane Smith", episodes = 58 }

With the new syntax, records are created by typing TVShow followed by a name, creator, and number of episodes. This syntax looks familiar, doesn’t it? It’s basically a function application. type alias created a function for constructing records in addition to giving the underlying structure a name.

It’s important to notice that when using type alias to define a record structure, we must use : not = to separate the property names and their types.

Accessing Values

Records are values like everything else in Elm. We can pass them to a function as an argument, modify them, and return them back. Let’s create a function that checks to see if a TV show record has a creator or not.

> hasCreator tvShow = String.length tvShow.creator > 0
<function>

> hasCreator firefly
True

> got = TVShow "Game of Thrones" "" 60
{ name = "Game of Thrones", creator = "", episodes = 60 }

> hasCreator got
False

The hasCreator function accesses the value in creator property using the dot syntax. Here are some more examples:

> firefly.name
"Firefly"

> firefly.creator
"Joss Whedon"

> firefly.episodes
14

Special Accessor Functions

The second way to access elements in a record is with a special function.

> .name firefly
"Firefly"

> .creator firefly
"Joss Whedon"

> .episodes firefly
14

.name, .creator, and .episodes are all special functions created behind the scenes when a record is created. These functions aren’t created by type alias though. They are available to us even if we use the literal syntax instead of a constructor function to create a record.

> wire = { name = "The Wire", creator = "David Simon", episodes = 60 }
{ name = "The Wire", creator = "David Simon", episodes = 60 }

> .name wire
"The Wire"

> .creator wire
"David Simon"

> .episodes wire
60

Why do we call them special functions? First, they start with a dot. Normal functions aren’t allowed to start with special characters. Second, they can only be used on a record that actually has a label that matches the name of a special function. Let’s try using them to access a value in a record that doesn’t have the same labels.

> sapiens = { name = "Sapiens", author = "Yuval Harari", published = 2015 }
{ name = "Sapiens", author = "Yuval Harari", published = 2015 }

> .creator sapiens

------------------ TYPE MISMATCH --------------------
The argument to this function is causing a mismatch.

8|   .creator sapiens
              ^^^^^^^
This function is expecting the argument to be:

    { b | creator : ... }

But it is:

    { author : ..., name : ..., published : ... }

What about .name though? Both records have a label called name.

> .name sapiens
"Sapiens"

That works because .name function isn’t tied to any of the records we created before sapiens. All it cares about is whether a record has a label called name or not. These special functions are essentially equivalent to:

.name = (\record -> record.name)

.creator = (\record -> record.creator)

.episodes = (\record -> record.episodes)

They take a record and return the value for the label represented by their name.

Sorting Records

What do we gain by having special functions for accessing values in a record? After all the regular dot syntax works just fine and even looks more natural. To understand their usefulness, let’s try to sort TV shows by the number of episodes they have.

> sortByEpisodes tvShow1 tvShow2 = compare tvShow1.episodes tvShow2.episodes
<function>

> List.sortWith sortByEpisodes [fringe, firefly, vice, wire]
...

Here’s how the output looks after some formatting:

[
  { name = "Firefly",
    creator = "Joss Whedon",
    episodes = 14
  },
  { name = "Vice",
    creator = "Shane Smith",
    episodes = 58
  },
  { name = "The Wire",
    creator = "David Simon",
    episodes = 60
  },
  { name = "Fringe",
    creator = "J. J. Abrams",
    episodes = 100
  }
]

Remember the sortWith function from the List module? We used it to sort a list of numbers in descending order like this:

descending a b =
    case compare a b of
        LT ->
            GT

        GT ->
            LT

        EQ ->
            EQ


List.sortWith descending [ 316, 320, 312, 370, 337, 318, 314 ]

sortWith accepts two arguments: a comparison function and a list that needs sorting. sortByEpisodes is our comparison function that takes two TV shows and simply compares the number of episodes in them. As it turns out the List module provides another function called sortBy that makes sorting records much easier.

> List.sortBy .episodes [ fringe, firefly, vice, wire ]
...

Here’s how the output looks after some formatting:

[
  { name = "Firefly",
    creator = "Joss Whedon",
    episodes = 14
  },
  { name = "Vice",
    creator = "Shane Smith",
    episodes = 58
  },
  { name = "The Wire",
    creator = "David Simon",
    episodes = 60
  },
  { name = "Fringe",
    creator = "J. J. Abrams",
    episodes = 100
  }
]

Instead of having to create a separate comparison function, we just tell sortBy which property to use while sorting records. sortBy then compares the specified properties and sorts a list accordingly. The first argument to sortBy must still be a function though. Since .episodes is a function, it’s perfectly fine to pass it to sortBy. Here’s another example of sortBy that sorts a list of strings by their length.

> List.sortBy String.length [ "Olivia", "Peter", "Walter", "Nina" ]
["Nina","Peter","Olivia","Walter"]

Mapping Records

In the List section, we saw how to use the map function to create a new list with the results of applying a given function on every element in a list. Since a list can also contain records in it, we can use map to transform those records into anything we like.

> List.map (\record -> record.name) [ firefly, fringe, vice, wire, got ]
["Firefly","Fringe","Vice","The Wire","Game of Thrones"]

We took a list of records and transformed it to a list of strings by applying an anonymous function that returns the value in name property to every record. We can actually make the map expression much nicer to read if we use the special function for accessing properties instead.

> List.map .name [ firefly, fringe, vice, wire, got ]
["Firefly","Fringe","Vice","The Wire","Game of Thrones"]

As you can see the special functions are quite useful when we want to pick just one property from each record and put them in a separate list.

Modifying a Record

Unlike tuples, we can modify values in a record. Remember all values in Elm are immutable and so are records. Because of that, Elm doesn’t really modify an existing record. It always returns a new one that contains the modified value. To understand how to modify a record, let’s create a function that increments the number of episodes by one.

> incrementEpisode tvShow = { tvShow | episodes = tvShow.episodes + 1 }
<function>

> firefly.episodes
14

> incrementEpisode firefly
{ name = "Firefly", creator = "Joss Whedon", episodes = 15 }

As we can see, the number of episodes has been incremented to 15. But if we print the episodes in firefly, it’s still 14.

> firefly.episodes
14

Elm didn’t change the record firefly points to. It returned a new one. The syntax for modifying a record looks a bit weird, doesn’t it? Let’s deconstruct it.

We can also modify mutiple properties at the same time.

> { fringe | creator = "Alex Kurtzman", episodes = fringe.episodes + 1 }
{ name = "Fringe", creator = "Alex Kurtzman", episodes = 101 }

The expressions for updating each property must be separated by commas. If the expression becomes too long to fit in one line, it’s perfectly fine to break it into multiple lines.

> { fringe | \
|   name = "Fringgge", \
|   creator = "Alex Kurtzman", \
|   episodes = fringe.episodes + 1 \
| }

Unlike all other data structures we’ve explored thus far, Elm doesn’t provide a separate module for records. That’s because other than the special accessors there are no functions available to manipulate records. Although that sounds limiting, a lot can be achieved just by using the special syntax for modifying records. Furthermore, the special accessor functions are generated on the fly after a record has been created. Therefore, we don’t need a separate module to house them.

Back to top

New chapters are coming soon!

Sign up for the Elm Programming newsletter to get notified!

* indicates required
Close