Tracks
/
Elm
Elm
/
Syllabus
/
Records
Re

Records in Elm

8 exercises

About Records

Records are data structures grouping together related information with labels. They are similar to objects in JavaScript or Java, or structs in C or Rust, but with some key distinctions.

Creating records

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

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

The type of a record is also defined with a similar syntax, except equal signs = are replaced by colon signs :.

firefly : { name : String, creator : String, episodes : Int }
firefly =
    { name = "Firefly"
    , creator = "Joss Whedon"
    , episodes = 14
    }

Repeating these type definitions is cumbersome, so using a type alias to share the definition is a common idiom.

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

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

The type alias also automatically supplies a constructor function for the record. This is sometimes surprising since functions in Elm must start with a lowercase character, but type constructors are one exception.

firefly : TvSeries
firefly = TvSeries "Firefly" "Joss Whedon" 14

Accessing record fields

The main way to access a field value of a record instance is to use the dot-notation such as firefly.creator. The Elm compiler also supports special accessor functions starting with a dot . such as .creator that will work on any record with a field of that name. That is the second exception to the rule that functions must start with a lowercase character. In fact user-defined functions must start with a lowercase character, but the two examples we just discovered are generated by the compiler, not the programmer.

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

firefly.name
    --> "Firefly"

.creator firefly
    --> "Joss Whedon"

Records can also be destructured in bindings using the record pattern matching. Destructuring works with any record that has fields of the relevant names.

episodesCount : TvSeries -> Int
episodesCount { episodes } =
    episodes

complicatedCopy : TvSeries -> TvSeries
complicatedCopy show =
    let
        { name, creator } = show
    in
    { name = name
    , creator = creator
    , episodes = episodesCount show
    }

Updating records

Records are immutable, as everything in Elm, so once a record is defined, its field values can never change. There is a special syntax to create a copy of an existing record, but changing one or more fields. This syntax uses the pipe symbol | to distinguish the original record and the fields that will change, such as { oldRecord | field1 = newValue }.

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

updatedFirefly : TvSeries
updatedFirefly =
    { firefly | creator = "J Whedon", episodes = 15 }

Comparing records

Elm uses [structural equality][equality], which means that two instances of the same record with identical values are equal.

firefly1 = TvSeries "Firefly" "Joss Whedon" 14
firefly2 = TvSeries "Firefly" "Joss Whedon" 14

firefly1 == firefly2
    --> True

Extensible records

Elm also supports structural typing meaning that if a function requires a record with an x and y field, it will work with any record that has those fields such as 2D points, 3D points, spaceships, etc. Those record types have a pipe | in their definition, such as { a | x : Float, y : Float } and are called "extensible records" in Elm terminology. Beware of the difference between a pipe | in a type definition, which is an extensible record definition ({ a | x : Int }), and a pipe | an actual record instance which means that we are updating some fields of that record.

point2d = { x = 1, y = 2 }
point3d = { x = 3, y = 4, z = 7 }

.x point2d --> 1
.x point3d --> 3

length : { a | x : Float, y : Float } -> Float
length vector =
    sqrt (vector.x * vector.x + vector.y * vector.y)

length point2d --> 2.236068
length point3d --> 5

translateX : { a | x : Float } -> { a | x : Float }
translateX vector =
    { vector | x = vector.x + 1 }

translateX point2d --> { x = 2, y = 2 }
translateX point3d --> { x = 4, y = 4, z = 7 }
Edit via GitHub The link opens in a new window or tab

Learn Records