Record Types

Records are types for grouping values together. They generalise the dependent product type by providing named fields and (optional) further components.

Example: the Pair type constructor

Record types can be declared using the record keyword

  1. record Pair (A B : Set) : Set where
  2. field
  3. fst : A
  4. snd : B

This defines a new type constructor Pair : Set → Set → Set and two projection functions

  1. Pair.fst : {A B : Set} Pair A B A
  2. Pair.snd : {A B : Set} Pair A B B

Note that the parameters A and B are implicit arguments to the projection functions.

Elements of record types can be defined using a record expression

  1. p23 : Pair Nat Nat
  2. p23 = record { fst = 2; snd = 3 }

or using copatterns. Copatterns may be used prefix

  1. p34 : Pair Nat Nat
  2. Pair.fst p34 = 3
  3. Pair.snd p34 = 4

suffix (in which case they are written prefixed with a dot)

  1. p56 : Pair Nat Nat
  2. p56 .Pair.fst = 5
  3. p56 .Pair.snd = 6

or using an anonymous copattern-matching lambda (you may only use the suffix form of copatterns in this case)

  1. p78 : Pair Nat Nat
  2. p78 = λ where
  3. .Pair.fst 7
  4. .Pair.snd 8

If you use the constructor keyword, you can also use the named constructor to define elements of the record type:

  1. record Pair (A B : Set) : Set where
  2. constructor _,_
  3. field
  4. fst : A
  5. snd : B
  6. p45 : Pair Nat Nat
  7. p45 = 4 , 5

In this sense, record types behave much like single constructor datatypes (but see Eta-expansion below).

Declaring, constructing and decomposing records

Declaring record types

The general form of a record declaration is as follows:

  1. record <recordname> <parameters> : Set <level> where
  2. <directives>
  3. constructor <constructorname>
  4. field
  5. <fieldname1> : <type1>
  6. <fieldname2> : <type2>
  7. -- ...
  8. <declarations>

All the components are optional, and can be given in any order. In particular, fields can be given in more than one block, interspersed with other declarations. Each field is a component of the record. Types of later fields can depend on earlier fields.

The directives available are eta-equality, no-eta-equality, pattern (see Eta-expansion), inductive and co-inductive (see Recursive records).

Constructing record values

Record values are constructed by giving a value for each record field:

  1. record { <fieldname1> = <term1> ; <fieldname2> = <term2> ; ... }

where the types of the terms match the types of the fields. If a constructor <constructorname> has been declared for the record, this can also be written

  1. <constructorname> <term1> <term2> ...

For named definitions, this can also be expressed using copatterns:

  1. <named-def> : <recordname> <parameters>
  2. <recordname>.<fieldname1> <named-def> = <term1>
  3. <recordname>.<fieldname2> <named-def> = <term2>
  4. ...

Records can also be constructed by updating other records.

Building records from modules

The record { <fields> } syntax also accepts module names. Fields are defined using the corresponding definitions from the given module. For instance assuming this record type R and module M:

  1. record R : Set where
  2. field
  3. x : X
  4. y : Y
  5. z : Z
  6. module M where
  7. x = ...
  8. y = ...
  9. r : R
  10. r = record { M; z = ... }

This construction supports any combination of explicit field definitions and applied modules. If a field is both given explicitly and available in one of the modules, then the explicit one takes precedence. If a field is available in more than one module then this is ambiguous and therefore rejected. As a consequence the order of assignments does not matter.

The modules can be both applied to arguments and have import directives such as hiding, using, and renaming. Here is a contrived example building on the example above:

  1. module M2 (a : A) where
  2. w = ...
  3. z = ...
  4. r2 : A R
  5. r2 a = record { M hiding (y); M2 a renaming (w to y) }

Decomposing record values

With the field name, we can project the corresponding component out of a record value. It is also possible to pattern match against inductive records:

  1. sum : Pair Nat Nat Nat
  2. sum (x , y) = x + y

Or, using a let binding record pattern:

  1. sum' : Pair Nat Nat → Nat
  2. sum' p = let (x , y) = p in x + y

Note

Naming the constructor is not required to enable pattern matching against record values. Record expressions can appear as patterns.

Record update

Assume that we have a record type and a corresponding value:

  1. record MyRecord : Set where
  2. field
  3. a b c : Nat
  4. old : MyRecord
  5. old = record { a = 1; b = 2; c = 3 }

Then we can update (some of) the record value’s fields in the following way:

  1. new : MyRecord
  2. new = record old { a = 0; c = 5 }

Here new normalises to record { a = 0; b = 2; c = 5 }. Any expression yielding a value of type MyRecord can be used instead of old. Using that records can be built from module names, together with the fact that all records define a module, this can also be written as

  1. new' : MyRecord
  2. new' = record { MyRecord old; a = 0; c = 5}

Record updating is not allowed to change types: the resulting value must have the same type as the original one, including the record parameters. Thus, the type of a record update can be inferred if the type of the original record can be inferred.

The record update syntax is expanded before type checking. When the expression

  1. record old { upd-fields }

is checked against a record type R, it is expanded to

  1. let r = old in record { new-fields }

where old is required to have type R and new-fields is defined as follows: for each field x in R,

  • if x = e is contained in upd-fields then x = e is included in new-fields, and otherwise

  • if x is an explicit field then x = R.x r is included in new-fields, and

  • if x is an implicit or instance field, then it is omitted from new-fields.

The reason for treating implicit and instance fields specially is to allow code like the following:

  1. data Vec (A : Set) : Nat Set where
  2. [] : Vec A zero
  3. __ : ∀{n} A Vec A n Vec A (suc n)
  4. record R : Set where
  5. field
  6. {length} : Nat
  7. vec : Vec Nat length
  8. -- More fields ...
  9. xs : R
  10. xs = record { vec = 0 1 2 [] }
  11. ys = record xs { vec = 0 [] }

Without the special treatment the last expression would need to include a new binding for length (for instance length = _).

Record modules

Along with a new type, a record declaration also defines a module with the same name, parameterised over an element of the record type containing the projection functions. This allows records to be “opened”, bringing the fields into scope. For instance

  1. swap : {A B : Set} Pair A B Pair B A
  2. swap p = snd , fst
  3. where open Pair p

In the example, the record module Pair has the shape

  1. module Pair {A B : Set} (p : Pair A B) where
  2. fst : A
  3. snd : B

Note

This is not quite right: The projection functions take the parameters as erased arguments. However, the parameters are not erased in the module telescope if they were not erased to start with.

It’s possible to add arbitrary definitions to the record module, by defining them inside the record declaration

  1. record Functor (F : Set Set) : Set where
  2. field
  3. fmap : {A B} (A B) F A F B
  4. _<$_ : {A B} A F B F A
  5. x <$ fb = fmap _ x) fb

Note

In general new definitions need to appear after the field declarations, but simple non-recursive function definitions without pattern matching can be interleaved with the fields. The reason for this restriction is that the type of the record constructor needs to be expressible using let-expressions. In the example below D₁ can only contain declarations for which the generated type of mkR is well-formed.

  1. record R Γ : Set where
  2. constructor mkR
  3. field f : A
  4. D
  5. field f : A
  6. mkR : {Γ} (f : A₁) (let D₁) (f : A₂) R Γ

Eta-expansion

The eta (η) rule for a record type

  1. record R : Set where
  2. field
  3. a : A
  4. b : B
  5. c : C

states that every x : R is definitionally equal to record { a = R.a x ; b = R.b x ; c = R.c x }. By default, all (inductive) record types enjoy η-equality if the positivity checker has confirmed it is safe to have it. The keywords eta-equality/no-eta-equality enable/disable η rules for the record type being declared.

Recursive records

Recursive records need to be declared as either inductive or coinductive.

  1. record Tree (A : Set) : Set where
  2. inductive
  3. constructor tree
  4. field
  5. elem : A
  6. subtrees : List (Tree A)
  7. record Stream (A : Set) : Set where
  8. coinductive
  9. constructor _::_
  10. field
  11. head : A
  12. tail : Stream A

Inductive records have eta-equality on by default, while no-eta-equality is the default for coinductive records. In fact, the eta-equality and coinductive directives are not allowed together, since this can easily make Agda loop. This can be overridden at your own risk by using the pragma ETA instead.

It is possible to pattern match on inductive records, but not on coinductive ones.

However, inductive records without η-equality do not support both matching on the record constructor and construction of record elements by copattern matching. It has been discovered that the combination of both leads to loss of subject reduction, i.e., reduction does not preserve typing. For records without η, matching on the record constructor is off by default and construction by copattern matching is on. If you want the converse, you can add the record directive pattern:

  1. record HereditaryList : Set where
  2. inductive
  3. no-eta-equality
  4. pattern
  5. field sublists : List HereditaryList
  6. pred : HereditaryList List HereditaryList
  7. pred record{ sublists = ts } = ts

If both eta-equality and pattern are given for a record types, Agda will alert the user of a redundant pattern directive. However, if η is inferred but not declared explicitly, Agda will just ignore a redundant pattern directive; this is because the default can be changed globally by option --no-eta-equality.

Instance fields

Instance fields, that is record fields marked with {{ }} can be used to model “superclass” dependencies. For example:

  1. record Eq (A : Set) : Set where
  2. field
  3. _==_ : A A Bool
  4. open Eq {{...}}
  1. record Ord (A : Set) : Set where
  2. field
  3. _<_ : A A Bool
  4. {{eqA}} : Eq A
  5. open Ord {{...}} hiding (eqA)

Now anytime you have a function taking an Ord A argument the Eq A instance is also available by virtue of η-expansion. So this works as you would expect:

  1. __ : {A : Set} {{OrdA : Ord A}} A A Bool
  2. x y = (x == y) || (x < y)

There is a problem however if you have multiple record arguments with conflicting instance fields. For instance, suppose we also have a Num record with an Eq field

  1. record Num (A : Set) : Set where
  2. field
  3. fromNat : Nat A
  4. {{eqA}} : Eq A
  5. open Num {{...}} hiding (eqA)
  6. _3 : {A : Set} {{OrdA : Ord A}} {{NumA : Num A}} A Bool
  7. x 3 = (x == fromNat 3) || (x < fromNat 3)

Here the Eq A argument to _==_ is not resolved since there are two conflicting candidates: Ord.eqA OrdA and Num.eqA NumA. To solve this problem you can declare instance fields as overlappable using the overlap keyword:

  1. record Ord (A : Set) : Set where
  2. field
  3. _<_ : A A Bool
  4. overlap {{eqA}} : Eq A
  5. open Ord {{...}} hiding (eqA)
  6. record Num (A : Set) : Set where
  7. field
  8. fromNat : Nat A
  9. overlap {{eqA}} : Eq A
  10. open Num {{...}} hiding (eqA)
  11. _3 : {A : Set} {{OrdA : Ord A}} {{NumA : Num A}} A Bool
  12. x 3 = (x == fromNat 3) || (x < fromNat 3)

Whenever there are multiple valid candidates for an instance goal, if all candidates are overlappable, the goal is solved by the left-most candidate. In the example above that means that the Eq A goal is solved by the instance from the Ord argument.

Clauses for instance fields can be omitted when defining values of record types. For instance we can define Nat instances for Eq, Ord and Num as follows, leaving out cases for the eqA fields:

  1. instance
  2. EqNat : Eq Nat
  3. _==_ {{EqNat}} = Agda.Builtin.Nat._==_
  4. OrdNat : Ord Nat
  5. _<_ {{OrdNat}} = Agda.Builtin.Nat._<_
  6. NumNat : Num Nat
  7. fromNat {{NumNat}} n = n