Guix: an intuition about G-expression

Preparing some materials for the First Workshop on Reproducible Software Environments for Research and High-Performance Computing, I am re-reading the G-Expressions section from the Guix manual. A must to read! And the presentation at FOSDEM by Chris Marusich came to my mind. Somehow, I borrowed some examples.

G-expressions make it easy to write to files Scheme code that refers to store items, or to write Scheme code to build derivations. This post is an attempt to build an intuition about them. Please take this post as a first introduction containing some approximations for clarity.

The vehicle for the journey is guix repl. Let start by binding the variable hi to the string "path/to/hi" and let append the string "/bin/bye".

scheme@(guix-user)> (define hi "path/to/hi")
scheme@(guix-user)> (begin (string-append hi "/bin/bye"))
$1 = "path/to/hi/bin/bye"

First, as an expression, the begin syntax is used to evaluate a sequence of sub-expressions in order. It could be omitted; from my point of view, it eases the reading.

Now consider that we do not want to fully evaluate the resulting string but we would like to construct an intermediate expression, which will be then evaluated. The quasiquote allows to protect the evaluation and unquote to escape this protection.

Scheme is able to control what is evaluated when constructing an expression and what is evaluated when computing this expression. It is about quasiquote and unquote; they are so familiar that they have their own syntactic sugar: backtick ( ` ) and comma ( , ). For instance,

scheme@(guix-user)> `(begin (string-append ,hi "/bin/bye"))
$2 = (begin (string-append "path/to/hi" "/bin/bye"))

Now, this expression can be evaluated later. Let say later is now:

scheme@(guix-user)> (eval $2 (interaction-environment))
$3 = "path/to/hi/bin/bye"

Here the term $2 refers to the interactive previous result. Please note that the expression is evaluated in the context of the (interaction-environment). So far, so good.

What if the variable refers to a package? Let check it. First, we import the packages from the module base and the variable hello corresponds to the package hello.

scheme@(guix-user)> (use-modules (gnu packages base))
scheme@(guix-user)> hello
$4 = #<package hello@2.12.1 gnu/packages/base.scm:90 7f0d0bba8dc0>

Let tweak the previous expression and replace the variable hi by the variable hello.

scheme@(guix-user)> `(begin (string-append ,hello "/bin/bye"))
$5 = (begin (string-append #<package hello@2.12.1 gnu/packages/base.scm:90 ...>
                           "/bin/bye"))

Interresting isn't it? Then, as previously, we are going to evaluate it.

scheme@(guix-user)> (eval $5 (interaction-environment))
ice-9/boot-9.scm:1685:16: In procedure raise-exception:
In procedure string-append: Wrong type (expecting string): #<package hello@2.12.1

Entering a new prompt.  Type `,bt' for a backtrace or `,q' to continue.
scheme@(guix-user) [1]> ,q

Bang! An error. Well, it was expected, no? The variable hello does not refer to a string so it does not make sense to append with one other string. G-expressions make it easy to write to files Scheme code that refers to store items. Let try that. Instead of quasiquote, let introduce gexp. And instead of unquote, let introduce ungexp. And similarly, they have their syntatic sugar:

quasiquote (backtick) ` #~ gexp
unquote (comma) , #$ ungexp

Before continuing, we need to load the module providing all this fun.

scheme@(guix-user)> (use-modules (guix gexp))

We are ready for replacing the two symbols:

scheme@(guix-user)> #~(begin (string-append #$hello "/bin/bye"))
$6 = #<gexp (begin (string-append #<gexp-input #<package hello@2.12.1
                                  "/bin/bye")) 7f0d0a9b6780>

Nice, it returns a G-expression type ( gexp ).

Warning: The following procedure is an helper for easing the explanations of this section. Please skip it elsewhere. It does not matter what it means or how to invoke it and, to my knowledge, it is not required for writing packages.

scheme@(guix-user)> (define gexp->sexp (@@ (guix gexp) gexp->sexp))

Once typed, consider it transforms from the G-expression type ( gexp ) to the regular S-expression type ( sexp ). And let be back to the explanations.

scheme@(guix-user)> (gexp->sexp $6 "x86_64-linux" #f)
$7 = #<procedure 7f0d0ac011e0 at guix/gexp.scm:1387:2 (state)>

Wait, the result is a procedure. And it seems that a state is required. What does it mean? Somehow, it needs some context (state) for making the evaluation sounds. How to evaluate? The interactive guix repl provides an handy ,run-in-store command – Guix specific REPL command; here the comma ( , ) is not related to unquote but to the command switch. Let run this procedure in the context of the Guix local store.

scheme@(guix-user)> ,run-in-store (gexp->sexp $7 "x86_64-linux" #f)
$8 = (begin
  (string-append
    "/gnu/store/8bzzc70vgzdvj6qdzhdpd709m4y2kw7z-hello-2.12.1"
    "/bin/bye"))

Bingo! We get a S-expression where the G-expression had been replaced by the value. Now we would be able to evaluate this S-expression and append the two strings.

G-expressions make it easy to manipulate Scheme code refering to store items.

Variation

In the very first example, the whole expression is quasiquoted and only the variable hi is unquoted. Why not unquote all the append? It would mean that the string append will be happened at construction time and not at evaluation time.

scheme@(guix-user)> `(begin ,(string-append hi "/bin/bye"))
$9 = (begin "path/to/hi/bin/bye")
scheme@(guix-user)> (eval $9 (interaction-environment))
$10 = "path/to/hi/bin/bye"

Similarly as previously, let just replace the symbols:

scheme@(guix-user)> #~(begin #$(string-append hello "/bin/bye"))

Again, as previously, it raises an error and it is expected. It does not make sense to append the type package with the type string.

ice-9/boot-9.scm:1685:16: In procedure raise-exception:
In procedure string-append: Wrong type (expecting string): #<package hello@2.12.1

Entering a new prompt.  Type `,bt' for a backtrace or `,q' to continue.
scheme@(guix-user) [1]> ,q

What is the solution? The solution is to not use the procedure string-append and instead rely on the procedure file-append which is designed for these kind of situations.

scheme@(guix-user)> #~(begin #$(file-append hello "/bin/bye"))
$11 = #<gexp (begin #<gexp-input #<file-append #<package hello@2.12.1
                                               "/bin/bye">:out>) 7f0d09225900>
scheme@(guix-user)> (gexp->sexp $11 "x86_64-linux" #f)
$12 = #<procedure 7f0d09393450 at guix/gexp.scm:1387:2 (state)>
scheme@(guix-user)> ,run-in-store $12
$13 = (begin
  "/gnu/store/8bzzc70vgzdvj6qdzhdpd709m4y2kw7z-hello-2.12.1/bin/bye")

Do you have a better intuition about G-expressions? Ready to dive G-expressions section of the Guix manual? Or The Store Monad section?

Join the fun, join Guix!


© 2014-2023 Simon Tournier <simon (at) tournier.info >

(last update: 2024-02-06 Tue 14:33)