It is possible to define a data type with field labels.
data Person = Person { age :: Int, name :: String }
This definition differs from a normal record definition as it also defines *record accessors which can be used to access parts of a data type.
In this example, two record accessors are defined, age
and name
, which allow us to access the age
and name
fields respectively.
age :: Person -> Int
name :: Person -> String
Record accessors are just Haskell functions which are automatically generated by the compiler. As such, they are used like ordinary Haskell functions.
By naming fields, we can also use the field labels in a number of other contexts in order to make our code more readable.
lowerCaseName :: Person -> String
lowerCaseName (Person { name = x }) = map toLower x
We can bind the value located at the position of the relevant field label whilst pattern matching to a new value (in this case x
) which can be used on the RHS of a definition.
NamedFieldPuns
lowerCaseName :: Person -> String
lowerCaseName (Person { name }) = map toLower name
The NamedFieldPuns
extension instead allows us to just specify the field label we want to match upon, this name is then shadowed on the RHS of a definition so referring to name
refers to the value rather than the record accessor.
RecordWildcards
lowerCaseName :: Person -> String
lowerCaseName (Person { .. }) = map toLower name
When matching using RecordWildCards
, all field labels are brought into scope. (In this specific example, name
and age
)
This extension is slightly controversial as it is not clear how values are brought into scope if you are not sure of the definition of Person
.
setName :: String -> Person -> Person
setName newName person = person { name = newName }
There is also special syntax for updating data types with field labels.