3.19

Record

Like tuples, records can also 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 }
{ creator = "Joss Whedon", episodes = 14, name = "Firefly" }

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

elm repl rearranges the position of each field in alphabetical order. Let’s assign the record above to a constant and while we’re at it create one more.

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

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

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

> tvShows = [ firefly, fringe ]
...

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

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

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 both records we just created a name.

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

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 "Joss Whedon" 14 "Firefly"
{ creator = "Joss Whedon", episodes = 14, name = "Firefly" }

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

With the new syntax, records are created by typing TVShow followed by creator, name, 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 defining a record structure using type alias, we must use : and not = to separate the fields from 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 "" 60 "Game of Thrones"
{ creator = "", episodes = 60, name = "Game of Thrones" }

> hasCreator got
False

The hasCreator function accesses the creator field’s value using the dot syntax. Here are some more examples:

> firefly.creator
"Joss Whedon"

> firefly.episodes
14

> firefly.name
"Firefly"

Special Accessor Functions

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

> .creator firefly
"Joss Whedon"

> .episodes firefly
14

> .name firefly
"Firefly"

.creator, .episodes, and .name 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 = { creator = "David Simon", episodes = 60, name = "The Wire" }
{ creator = "David Simon", episodes = 60, name = "The Wire" }

> .creator wire
"David Simon"

> .episodes wire
60

> .name wire
"The Wire"

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 field that matches the name of the special function. Let’s try using them to access a field in a record that doesn’t have the same name.

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

> .creator sapiens

-------------------- TYPE MISMATCH --------------------
The 1st argument to this function is not what I expect:

12|   .creator sapiens
               ^^^^^^^
This `sapiens` value is a:

    { author : String, name : String, published : number }

But this function needs the 1st argument to be:

    { b | creator : a }

What about .name though? Both records have a field 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 field called name or not. These special functions are equivalent to:

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

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

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

They take a record and return the value for the field 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, wire ]
...

Here’s how the output looks after some formatting:

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

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, wire ]
...

Here’s how the output looks after some formatting:

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

Instead of having to create a separate comparison function, we just tell sortBy which field to use while sorting records. sortBy then compares the specified field values 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

Earlier in the List section, we saw how to use the map function to transform a given list by applying a function to every element in that 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, wire, got ]
["Firefly","Fringe","The Wire","Game of Thrones"]

We took a list of records and transformed it to a list of strings by applying an anonymous function to each element. The anonymous function simply returns the value in the name field. We can actually replace the anonymous function with the special function for accessing fields.

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

As you can see the special functions are quite useful when we want to pick just one field 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
{ creator = "Joss Whedon", episodes = 15, name = "Firefly" }

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 multiple fields at the same time.

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

The expressions for updating each field 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 | \
|   creator = "Alex Kurtzman", \
|   episodes = fringe.episodes + 1, \
|   name = "Fringgge" \
| }

Note: 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 syntax for modifying records. Furthermore, the special accessor functions are generated on the fly when a record is created. Therefore, we don’t need a separate module to house them.

Back to top
Close