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!