"Guix drops" part 3: G-expressions

by Marius Bakke — Tue 29 November 2022

This is the third post in a series showcasing advanced features of Guix. Previous posts can be found below:

If you've used Guix a while you may have heard of G-expressions. You may have even used them. But what are they? The inetd system test says it best:

Like an S-expression but with a G.

Even as a seasoned Guix developer, when G-expressions were introduced it took me a long time before having that "gee, I'm starting to understand how this all works" moment. Becoming proficient at it took even longer.

"Give me a break, I've only been working here five years!"

The academically inclined may want to check out this paper for an in-depth introduction. I'll try to break the concepts down a little for laypeople or learning challenged (like myself).

(Be sure to also check out the Guix manual)

I think of G-expressions as a scripting language with the usual niceties of Guix: reproducible and cached computations. It's an extension of the Scheme language that brings the power of all 20k+ packages in Guix with it.

Here is an example from my system configuration:

(use-modules (guix gexp)
             (gnu artwork)
             (gnu packages inkscape))

(define background
  (computed-file
   "guix-background.png"
   #~(begin
       (let ((inkscape #+(file-append inkscape "/bin/inkscape"))
             (backgrounds #$(file-append %artwork-repository "/backgrounds")))
         (system* inkscape (string-append "--export-filename=" #$output)
                  (string-append backgrounds "/guix-silver-checkered-16-9.svg")
                  "-w" "2560" "-h" "1440")))))

This computes a nice PNG file from the raw SVG logo found in the guix artwork repository.

This computed PNG is then used in my window manager configuration:

(define %sway-config
  (mixed-text-file
   "sway-config"
[...]
### Output configuration
#
# Wallpaper
output * bg " background " fill

mixed-text-file is an N-argument procedure that creates a text file from the strings and variables passed as arguments. A more gexp-y way to declare the configuration would be to use computed-file like so:

(define %sway-config
  (computed-file
   "sway-config"
   #~(call-with-output-file #$output
[...]
# Wallpaper
output * bg #$background fill

In both cases the result is the same: the background we computed above is inserted into the configuration file with the full /gnu/store/... file name.

If the SVG file (or Inkscape) is updated, the background will be computed anew, fully transparent to the configuration. Otherwise the cached result is used.

This makes for a very powerful scripting language where all the programs you need are "already installed", and each computation is cached. If any of the inputs (e.g. packages) to the script change, the cache is automatically invalidated.

The gexp machinery works "out of the box" in the context of guix build, inside manifests, etc. It took me a while to figure out how to access this in general-purpose scripts. Here is a toy example that calculates the factorial of the given argument and saves the result in the store:

#!/usr/bin/env -S guile --no-auto-compile
;; -*- mode: Scheme;-*-
!#

(use-modules (guix gexp)
             (guix derivations)
             (guix store))

(define (calculate-factorial)
  (let ((argument (cadr (command-line))))
    (gexp->derivation (string-append "factorial-of-" argument)
      #~(begin
          (use-modules (ice-9 format))
          (define (factorial n)
            (if (zero? n)
                1
                (* n (factorial (- n 1)))))
          (let ((result (factorial (string->number #$argument))))
            (call-with-output-file #$output
              (lambda (port)
                (format port "~d~%" result))))))))

(let* ((store (open-connection))
       (drv (run-with-store store (calculate-factorial)))
       (output (derivation->output-path drv)))
  (build-derivations store (list drv))
  (format #t "~a~%" output))

It can be run with guile toy.scm or made executable and run directly:

$ ./toy.scm 42
/gnu/store/brmr07dx7mhrm9f0wfic2rvx22dgyrzz-factorial-of-42
$ cat $(!!)
1405006117752879898543142606244511569936384000000000

Calculating the factorial of 25000 took 4 seconds on my machine. Running the same command again hit the cache and took 0.7 seconds.