A deep dive into Guix records
by Julien Lepiller — dim. 17 juillet 2022
The records are an essential part of the Guix API. Many objects manipulated by Guix are records: from packages to operating-systems, origins, service configuration, manifests, etc. So what are they, and do you use them?
This article is an attempt to give you a more in-depth understanding of what a record is and what you can and cannot do with them. But first, what's a record and why is it called that way?
Records in Guile
In programming languages, especially functional programming languages, records are a common data structure. A record is way to group data together. It is a sort of key-value structure where each key (called a field) is associated with a value.
Now, a record does not accept any kind of fields. Before you can create a record, you need to define a record-type. The record type basically dictates what fields are allowed in the record. In fact, it lists all the fields that must be present in a record of this type.
Guile provides records and record-types in its (srfi srfi-9)
module. To
declare a new record type, you can use:
(use-modules (srfi srfi-9))
(define-record-type foo
(make-foo field1 field2 field3)
foo?
(field1 foo-field1)
(field2 foo-field2)
(field3 foo-field3))
Now let's decompose this a bit.
First, we are calling a macro, define-record-type
that… well… defines a new
record type. This type is called foo
and the macro accepts three types of
arguments:
- The constructor,
(make-foo field1 field2 field3)
. It creates a new procedure with the same name as the first element, somake-foo
in our case. By convention, the constructor is always calledmake-<name-of-record-type>
. Then, it is followed by the names of the fields in the record, herefield1
tofield3
. - A predicate name,
foo?
. This creates a new procedure with that name, that takes a single argument and returns#t
only if it is a record of the type that is being defined. - A list of field declarations. Each field is composed of the name of the field and an accessor name for that field. The accessor name creates a new procedure with that name, that takes a single argument and returns the value associated with the field being defined in its argument, if the argument is a record of the type being defined.
OK, this might have been confusing already. To summarize, define-record-type
defines:
- A constructor, to create a record of that type,
- A predicate, to check whether a record is of that type,
- Accessors, to retrieve values associated with the fields of a record.
To illustrate, let's first define a record that does something useful:
(define-record-type <point>
(make-point x y)
point?
(x point-x)
(y point-y))
As we said, we can create a record of that type by using the constructor that was defined. The constructor takes exactly the same arguments as the ones that are declared in the record type, so creating a point looks like this:
(define my-point (make-point 1 5))
;; scheme@(guile-user)> my-point
;; $1 = #<<point> x: 1 y: 5>
As you can see, we have created a point that holds coordinates. Using the REPL, we can see the values associated with the fields. Now let's see how to get these coordinates back:
(display (point-x my-point))
(newline)
;; 1
(display (point-y my-point))
(newline)
;; 5
And we can check that my-point
is indeed a point with the predicate we
defined:
(if (point? my-point)
(display "yes\n")
(display "no\n"))
;; yes
So this is the basic knowledge you need to have about records in Guile. Some more observations can be made though, and we're going to explore some of the points that might be confusing when you start learning about records in the next sections.
For the rest of this section, I will need the following types:
;; pretty much the same as <point> above, but with one more coordinate
(define-record-type <3d-point>
(make-3d-point x y z)
3d-point?
(x 3d-point-x)
(y 3d-point-y)
(z 3d-point-z))
Record types are disjoint
What I mean by that is that if you have two record types, a record can only be
one of these two types, not both at the same time. So in our example, a <point>
is not a <3d-point>
and conversely, a <3d-point>
is not a <point>
:
(define my-other-point (make-3d-point 5 9 6))
;; scheme@(guile-user)> (point? my-other-point)
;; $1 = #f
;; scheme@(guile-user)> (3d-point? my-other-point)
;; $2 = #t
;; scheme@(guile-user)> (point? my-point)
;; $3 = #t
;; scheme@(guile-user)> (3d-point? my-point)
;; $4 = #f
Accessors are type-specific
What I mean by that is that you can't use an accessor defined in a type to access a field in another record, even if it has the same name, or if it is at the same position:
;; scheme@(guile-user)> (point-x my-other-point)
;; ice-9/boot-9.scm:1685:16: In procedure raise-exception:
;; In procedure point-x: Wrong type argument: #<<3d-point> x: 5 y: 9 z: 6>
That is because you're trying to access field x of record type <point>
, but
you give it a <3d-point>
.
Playing with records
So, now that we understand how record work and don't work, let's try a little exercise. Let's try defining a procedure to addition two points. If you don't know what that means, let's just say we want a procedure that takes two points a returns a new points whose coordinates are the sum of coordinates of the two points. So adding point with coordinate (1,5) with point (4,-2) gives a point with coordinates (5,3). Sounds easy?
The solution will be a procedure, say add-points
that takes two <points>
,
like this:
(define (add-points p1 p2)
;; do something...
)
With what we learned above, you should be able to write that procedure by yourself. Give it a try!
Here's a possible solution:
(define (add-points p1 p2)
(make-point
(+ (point-x p1) (point-x p2)) ; X coordinate
(+ (point-y p1) (point-y p2)))) ; Y coordinate
Record type modifiers
In reality, Guile records can also add modifiers to their field declarations. We are not covering them here, you can read the manual for more information if you'd like.
Guix records
Now that we've seen the basics of records in Guile, let's talk about Guix records.
Confusingly, Guix also defines its own types of record types. (guix records)
is the module you need to create new Guix record types.
Note: from now on we are going to use Guix records. If you want to follow
what we are doing here, you should open a new guix repl
. We're going to
define records with the same name as above. Make sure to start from a new
REPL or you could get some confusing error messages later on.
Defining a Guix record (from now own, a record) type is very similar to Guile records, so for a simple point like we did above:
(use-modules (guix records))
(define-record-type* <point> point
make-point
point?
(x point-x)
(y point-y))
First, you'll notice that we use a different macro to define this record type,
define-record-type*
. The *
is conventionally used to mean “same but
different”. It does the same thing, but slightly differently.
First, there are more things declared at the beginning of the record type:
- The name: as before, it's
<point>
. For these kind of record types, it is usually wrapped into<>
. - The syntactic constructor, it's
point
. This is the constructor you are going to interact with most of the time. - The constructor, it's
make-point
. This is the same constructor as for Guile records, but you're not going to interact with it. Notice that this time, it is not necessary to re-declare all the field names inside. We just need to define a name for the constructor. - The predicate, it's
point?
. This is the same as the predicate for Guile records. - The fields, similar to Guile records.
So, compared to Guile records, the main difference is the syntactic constructor. This is how you use it:
(define my-point
(point
(x 1)
(y 5)))
;; scheme@(guile-user)> my-point
;; $1 = #<<point> x: 1 y: 5>
The syntactic constructor makes the field names explicit. How is that useful you ask? For big records, it's quite handy to be able to name records like that. You can also reorder them, like so:
(define my-point
(point
(y 5)
(x 1)))
;; scheme@(guile-user)> my-point
;; $1 = #<<point> x: 1 y: 5>
Not very impressive though. The main advantage of Guix records are their additional features, that we are going to study now.
Default values
Guix record types can declare a default value for a field. This can look like this:
(define-record-type* <point> point
make-point
point?
(x point-x
(default 0))
(y point-y
(default 2)))
Here, the default value for x
is 0, and the default value of y
is 2. Now that
we declared a default value, we can omit these fields in the definition of a
record of this type:
(define my-point
(point)) ;; no field, all of them keep their default value
;; #<<point> x: 0 y: 2>
(define my-point
(point
(x 18)))
;; #<<point> x: 18 y: 2>
(define my-point
(point
(y -7)))
;; #<<point> x: 0 y: -7>
Refer to other fields
When constructing a record, you can refer to the value associated with a field you defined above. For instance:
(define my-point
(point
(x 14)
(y (+ x 2))))
;; #<<point> x: 14 y: 16>
Thunked fields
When defining a new record type, you can make a field thunked. This means that accessing the field's value will compute it in the dynamic extent of the caller. This is mostly used with fluids in Guix, as well as a special this reference (see below).
To illustrate with fluids:
(define %current-x (make-fluid 0))
(define-record-type* <point> point
make-point
point?
(x point-x (thunked))
(y point-y (default 0)))
(define my-point
(point
(x (%current-x))))
;; scheme@(guile-user) (point-x my-point)
;; $1 = 0
(fluid-set! %current-x 17)
;; scheme@(guile-user) (point-x my-point)
;; $2 = 17
This reference
When defining a new record type, you can add another definition before the list
of fields, a this-identifier. It is a simple name, usually something like
this-<name-of-the-record-type>
. It can be used in thunked fields to refer to
the current record. For instance, this can be useful to define default values.
Here is an example of a point that has the same x and y coordinates by default:
(define-record-type* <point> point
make-point
point?
this-point
(x point-x (default (point-y this-point)) (thunked))
(y point-y (default (point-x this-point)) (thunked)))
(define my-point
(point
(x 5)))
;; scheme@(guile-user)> (point-x my-point)
;; $1 = 5
;; scheme@(guile-user)> (point-y my-point)
;; $2 = 5
Note that the example above is very silly and a bad idea, because field x refers to field y and field y refers to field x, by default. So if you use the default values for x and y, accessing any of these points generates an infinite loop. Don't try this at home!
Inheritance
Inheritance can be used to create a new record that uses the same values as
another record of the same type, while overriding some of the fields. To use
inheritance, when you create a new record, you use the pseudo-field inherit
.
It needs to be the first field in the record declaration. Here's an example:
(define-record-type* <point> point
make-ponit
point?
(x point-x) ;notice: no default value
(y point-y)) ; notice: no default value
(define my-first-point
(point
(x 1)
(y 3)))
(define my-second-point
(point
(inherit my-first-point) ; all undeclared fields here are copied from my-first-point
(y 5)))
(define my-third-point
(point
(inherit my-first-point) ; all undeclared fields here are copied from my-first-point
(x 4)))
(define my-fourth-point
(point
(inherit my-second-point) ; all undeclared fields here are copied from my-second-point
(x 8)))
;; scheme@(guile-user)> my-first-point
;; #<<point> x: 1 y: 3>
;; scheme@(guile-user)> my-second-point
;; #<<point> x: 1 y: 5>
;; scheme@(guile-user)> my-third-point
;; #<<point> x: 4 y: 3>
;; scheme@(guile-user)> my-fourth-point
;; #<<point> x: 8 y: 5>
Also note that you cannot refer to a field you did not declare in the current syntactic constructor. Here's an invalid example:
(define my-fith-point
(inherit my-first-point)
(x y))
;; ice-9/boot-9.scm:1685:16: In procedure raise-exception:
;; error: y: unbound variable
Innate fields
An innate field is a field that cannot be inherited. This is useful in Guix for location fields: they can refer to the location of the declaration instead of inheriting the location from the parent declaration. Here's an example of that in action:
(define-record-type* <point> point
make-point
point?
(x point-x (default 0))
(y point-y (default 0))
(loc point-location (innate) (default (current-source-location))))
(define p
(point
(x 5)))
;; #<<point> x: 5 y: 0 loc: ((line . 23) (column . 10))>
(define p2
(point
(inherit p)
(x 4)))
;; #<<point> x: 4 y: 0 loc: ((line . 25) (column . 10))>
Note how the location (esp. the line number) changed between the two: this is
because the default value is used again (and re-computed) when declaring p2
,
even though it inherits from p
. If the loc
field were not declared as innate,
it would be inherited and the two locations would have been the same.
Delayed fields
A delayed field is used to automatically wrap the value of the field into
(delay ...)
. In Guix, this is used in the patches field of the origin record
type. This ensures that patches are retrieved lazily, as needed, so they are
not searched for everytime all the packages are accessed (eg. during package search).
It works in a similar fashion to thunked
fields, but does not allow reference
to this.
(define-record-type* <point> point
make-point
point?
(x point-x (delayed))
(y point-y))
(define p1 (make-point (x 1) (y (begin (sleep 5) 2))))
;; wait 5 seconds because y is not delayed
(define p2 (make-point (x (begin (sleep 5) 1)) (y 5)))
;; returns immediately because x is delayed
(point-x p2)
;; wait 5 seconds for the value to be computed
(point-x p2)
;; no more waiting, the value was computed previously
As you can see, once the value is computed, it is recorded and not computed again the next time you want to access it. This is another difference with thunk fields that will recompute every time you access them.
Sanitizer
Finally, sanitizers are procedures attached to a field that will modify the declared value. For instance, this could be used to accept more data types and convert them to a unique type, so procedures that use a record of this type do not have to manage multiple cases for the different supported types.
Here's an example where points accept numbers, and convert them to integers by rounding floating point and exact numbers.
(define (number->integer value)
(inexact->exact (round value)))
(define-record-type* <point> point
make-point
point?
(x point-x (sanitize number->integer))
(y point-y (sanitize number->integer)))
(define p
(point
(x 1.5)
(y (/ 3 7))))
;; #<<point> x: 2 y: 0>
Conclusion
Now you know everything there is to know about Guix records! Happy Guix hacking!