elm christmas

Extensible records

←Previous postNext post →

You won't belive what these records can do!

A 4 min read written by
Jonas Berdal

One of the lesser known features of Elm is that of extensible records. The reason you might not have heard about this before is that there are few practical use cases where extensible records is clearly the best solution. Even the official Elm documentation introduces this feature in a modest way: This [extensible records] use has not come up much in practice so far, but it is pretty cool nonetheless.

So, what is an extensible record?

An extensible record is a type that has at least certain fields, but may have others as well. Its type definition looks a lot like a normal type definition for a record:

type alias ChristmasType a =
    { a
        | christmasValue : Int
        , name : String
    }

This type defintion describes some polymorphic record a with fields christmasValue and name. We can now define records using the extensible record:

--The normal way defining a record type alias
type alias ChristmasSong =
    { name : String, band : String, christmasValue : Int }

type alias ChristmasCharacter =
    { name : String, christmasValue : Int, weight : Int }

--Defining a record type alias using our extensible record
type alias ChristmasSong =
    ChrismasType { band : String }

type alias ChristmasCharacter =
    ChrismasType { weight : Int }

Here are some examples of these record types:

fairyTale : ChristmasSong
fairyTale =
    { name = "Fairytale of New York", band = "The Pogues", christmasValue = 95 }

santaClaus : Character
santaClaus =
    { name = "Nicholas", weight = 120, christmasValue = 100 }

Here we see that the records fairyTale and santaClaus have different fields, and therefore different types, but that they are both extensions of ChristmasType since they have the required fields. Note that we can not put these different records into a list together since a list can only contain items of one polymorphic type a at the same time. We can now define functions using our extendable record type:

christmasModeText : ChristmasType a -> String
christmasModeText item =
    String.join ""
        [ item.name
        , " puts me "
        , String.fromInt item.christmasValue
        , "% in christmas mode."
        ]

biggestChristmasValue : ChristmasType a -> ChristmasType b -> String
biggestChristmasValue itemA itemB =
    if itemA.christmasValue > itemB.christmasValue then
        itemA.name

    else
        itemB.name

These two functions work on any records that are extensions of ChristmasType. Notice that the biggestChristmasValue function accepts arguments of potentially different polymorphic types a and b. This allows us to compare different types of extensions, just like santaClaus and fairyTale:

biggestChristmasValue fairyTale santaClaus  --"Nicholas"

When should you use extensible records?

If you have lots of different records with common fields, extensible records might be a handy tool. Lets say you are making a game where all elements in the game are represented by records. In this case an extensible record might be a good option for calculating distance between any two elements:

type alias PositionedElement a =
    { a
        | x : Float
        , y : Float
    }

calculateDistance : PositionedElement a -> PositionedElement b -> Float

santa : PositionedElement { gifts : List Gift }

house : PositionedElement { wantedGift: Gift }

calulateDistance santa house

However, this problem could also be solved nicely without using extensible records:

type alias Position  =
    { x : Float
    , y : Float
    }

calculateDistance : Position -> Position -> Float

santa : { position: Position, gifts : List Gift}

house : { position: Position, wantedGift: Gift }

calulateDistance santa.position house.position

Another use case for extensible records is narrowing of types. If we have a large model with many fields, we can use extensible record to narrow what types we can access in a function, and achieve some of the same benefits we had with opaque types:

type alias Model  =
    { song : ChristmasSong
    , food : ChristmasFood
    , guests : List ChristmasGuest
    , motherInLaw : Maybe ChristmasGuest
    , theGrinch : Maybe ChristmasGuest
    , decorations : List ChristmasDecoration
    , mood : ChristmasSpiritLevel
    , year : Int
    , time : Time.Posix
    }

type alias Guests a :
    { a
        | guests : List ChristmasGuest
        , motherInLaw : Maybe ChristmasGuest
        , theGrinch : Maybe ChristmasGuest
    }

predictNumberOfPresents: Guests a -> Int

The advantage with this approach over manually picking out relevant fields is that we limit the number of arguments to the function, and that we can collect relevant fields into meaningfull abstractions. The advantage over nesting the record is that we can reuse the same field in several abstractions.

Tomorrow we will see how the popular package elm-css uses extensible records in an ingenious way to make css type safe.

←Previous postNext post →