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.
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
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
}
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 }
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
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 }