# Record Syntax
# Basic Syntax
Records are an extension of sum algebraic data
type that allow fields to be named:
data StandardType = StandardType String Int Bool --standard way to create a product type
data RecordType = RecordType -- the same product type with record syntax
{ aString :: String
, aNumber :: Int
, isTrue :: Bool
}
The field names can then be used to get the named field out of the record
> let r = RecordType {aString = "Foobar", aNumber= 42, isTrue = True}
> :t r
r :: RecordType
> :t aString
aString :: RecordType -> String
> aString r
"Foobar"
Records can be pattern matched against
case r of
RecordType{aNumber = x, aString=str} -> ... -- x = 42, str = "Foobar"
Notice that not all fields need be named
Records are created by naming their fields, but can also be created as ordinary sum types (often useful when the number of fields is small and not likely to change)
r = RecordType {aString = "Foobar", aNumber= 42, isTrue = True}
r' = RecordType "Foobar" 42 True
If a record is created without a named field, the compiler will issue a warning, and the resulting value will be undefined
.
> let r = RecordType {aString = "Foobar", aNumber= 42}
<interactive>:1:9: Warning:
Fields of RecordType not initialized: isTrue
> isTrue r
Error 'undefined'
A field of a record can be updated by setting its value. Unmentioned fields do not change.
> let r = RecordType {aString = "Foobar", aNumber= 42, isTrue = True}
> let r' = r{aNumber=117}
-- r'{aString = "Foobar", aNumber= 117, isTrue = True}
It is often useful to create lenses (opens new window) for complicated record types.
# Defining a data type with field labels
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.
# Pattern Matching
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.
# Pattern Matching with 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.
# Pattern Matching with 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
.
# Record Updates
setName :: String -> Person -> Person
setName newName person = person { name = newName }
There is also special syntax for updating data types with field labels.
# Copying Records while Changing Field Values
Suppose you have this type:
data Person = Person { name :: String, age:: Int } deriving (Show, Eq)
and two values:
alex = Person { name = "Alex", age = 21 }
jenny = Person { name = "Jenny", age = 36 }
a new value of type Person
can be created by copying from alex
, specifying which values to change:
anotherAlex = alex { age = 31 }
The values of alex
and anotherAlex
will now be:
Person {name = "Alex", age = 21}
Person {name = "Alex", age = 31}
# Records with newtype
Record syntax can be used with newtype
with the restriction that there is exactly one constructor with exactly one field. The benefit here is the automatic creation of a function to unwrap the newtype. These fields are often named starting with run
for monads, get
for monoids, and un
for other types.
newtype State s a = State { runState :: s -> (s, a) }
newtype Product a = Product { getProduct :: a }
newtype Fancy = Fancy { unfancy :: String }
-- a fancy string that wants to avoid concatenation with ordinary strings
It is important to note that the record syntax is typically never used to form values and the field name is used strictly for unwrapping
getProduct $ mconcat [Product 7, Product 9, Product 12]
-- > 756
# RecordWildCards
{-# LANGUAGE RecordWildCards #-}
data Client = Client { firstName :: String
, lastName :: String
, clientID :: String
} deriving (Show)
printClientName :: Client -> IO ()
printClientName Client{..} = do
putStrLn firstName
putStrLn lastName
putStrLn clientID
The pattern Client{..}
brings in scope all the fields of the constructor Client
, and is equivalent to the pattern
Client{ firstName = firstName, lastName = lastName, clientID = clientID }
It can also be combined with other field matchers like so:
Client { firstName = "Joe", .. }
This is equivalent to
Client{ firstName = "Joe", lastName = lastName, clientID = clientID }