A Taste of (Guile) Scheme
Introduction
This post gives a short introduction to Guile Scheme and some of its built-in modules. It may be viewed as a prequel to my recent post about Guile's exception system.
Why would anyone use Scheme in 2026? Its ecosystem is tiny and fragmented, and apart from guix there are few notable applications using Scheme. MIT's switch from Scheme to Python in 2009 acknowledged Python's rapid ascent to the top of the programming language popularity charts.
I use Scheme as a learning device to explore protocols and algorithms (for example, HTTP/2, gRPC, QUIC, symbolic math, type theory). First, Scheme's large but incomplete libraries give me an excuse to build a solution from scratch, and that's still one of the best ways to develop a deep understanding of a technical topic. Second, Scheme combines a small and consistent core with support for almost any programming paradigm (functional, precedural, object-oriented, meta-programming). This allows me to explore various approaches without ever hitting the limits of the language.
These experiments made me realize that my impression of Scheme as this small academic language was completely outdated. A modern Scheme implementation goes way beyond the "lispy" core and covers files, networking, multi-threading, process control, text processing, and many more areas. Guile in particular is one of the most comprehensive and approachable Scheme implementations, especially on macOS and Linux. Besides the broad library support, its foreign function interface makes it easy to fill any gaps by adding a Guile interface to an existing C library.
I'm not assuming any prior Scheme knowledge, but my explanations may be too terse to serve as a proper introduction to Scheme. I recommend looking at other resources in parallel and asking your favorite LLM for help and context.
I'm going to present everything by example using either the interactive shell (the "REPL") or unit tests that assert the results. Most features should be clear enough from these examples and the short descriptions. If not, the Guile reference manual is only one click away. If you are using Emacs, you can consult the manual via info (C-h R guile) right in your programming environment.
Examples
Let's start with a few example demonstrating that Scheme is an expressive language for everyday tasks. I will not explain these examples in detail here, but by the end of this post they should be easy to follow.
Of course we have to start with the obligatory "Hello World!" program.
(display "Hello World!\n")We will explain the overall syntax below, but apart from the parentheses this program should be clear enough. The following program writes this message 50 times:
(do ((i 0 (1+ i)))
((= i 50))
(display "Hello World!")
(newline))More parentheses! The (i 0 (1+ i) expression sets the variable i to 0 at the beginning and increments it by 1 at the end of every iteration. The (= i 50) causes the loop to stop when i reaches 50 so that the display statement executes 50 times. You can interpret the do expression as a "while not" loop.
The following example lists the files in the current directory.
(use-modules (ice-9 ftw))
(scandir "." (lambda (file)
(format #t "file: ~a~%" file)))The first line imports the ftw (file tree walker) module which contains the scandir function. This function scans the given directory and passes each filename to a function. The lambda creates this function (you can read the lambda as "function"). The format function formats a string with placeholders (similar to printf). The ~a is the placeholder for any object, and ~% prints a newline. The #t is Scheme's symbol for the Boolean "true" and in this case tells Scheme to write the formatted string to the standard output.
The following program counts the number of lines that contain a given string.
(use-modules (ice-9 rdelim))
(define (count-lines-containing-string port str)
(let loop ((count 0))
(let ((line (read-line port)))
(if (eof-object? line)
count
(loop (+ count (if (string-contains line str) 1 0)))))))
(define (main args)
(if (= (length args) 3)
(let ((filename (cadr args))
(str (caddr args)))
(call-with-input-file filename
(lambda (port)
(format #t "count=~a~%" (count-lines-containing-string port str)))))))
(main (command-line))Even though you might not be able to guess how every single line works, it's still fairly obvious where the command line arguments are read, where the file is opened, and where each line in the file is checked.
Another example straight from the Guile reference manual demonstrates a "Hello World!" web server running on localhost:8080:
(use-modules (web server))
(define (hello-world-handler request request-body)
(values '((content-type . (text/plain)))
"Hello World!\n"))
(run-server hello-world-handler)We can test the server with curl localhost:8080 or using the Guile HTTP client module:
(use-modules (web client))
(use-modules (ice-9 receive))
(receive (response body) (http-request "http://localhost:8080")
(format #t "response: ~a~%body: ~a~%" response body))As a slightly more interesting web client example, the following program calls the openlibrary JSON API to fetch the top 5 programming books.
(use-modules (web client)
(web response)
(json)
(ice-9 receive)
(ice-9 match)
(rnrs bytevectors)
((srfi srfi-43) #:select (vector-for-each)))
(define (fetch-books-by-subject subject)
(let ((url (string-append "https://openlibrary.org/subjects/" subject ".json?limit=5")))
(receive (response body)
(http-get url #:headers '((user-agent . "Guile-Web-Client-Example/1.0")))
(if (= (response-code response) 200)
(let* ((json-str (if (string? body) body (utf8->string body)))
(data (json-string->scm json-str))
(works (assoc-ref data "works")))
(format #t "Top books for subject '~a':~%~%" subject)
(vector-for-each
(lambda (i work)
(let* ((title (assoc-ref work "title"))
(author (match (assoc-ref work "authors")
(#((("name" . author-name) . _) _ ...) author-name)
(_ "Unknown author"))))
(format #t "~a. ~a: ~a~%" (1+ i) author title)))
works))
(format #t "Request failed with HTTP status: ~a~%" (response-code response))))))
(fetch-books-by-subject "programming")There is a bit too much going on to explain every single line, but parentheses aside, one can still recognize the overall structure of the program as it fetches the response from the web service and matches the JSON payload to extract title and author of each book. Overall, the program is surprisingly short.
I hope that these example pique your curiosity enough to keep reading.
Exploring Guile Scheme interactively
When we call guile without any arguments, we are entering Guile's interactive shell, also known as the read-eval-print-loop or REPL because that's what it does: It reads an expression, evaluates it, and prints the result in a loop.
Preparing the REPL
By default this "shell" lacks many features you may expect from a real command line shell such as auto-completion, keyboard shortcuts for fast navigation and editing on the command line, and command line history. We can change that by putting the following contents into Guile's configuration file .guile in our home directory:
(use-modules (ice-9 readline))
(activate-readline)This enables the readline functionality with keyboard navigation and command line history. The key bindings depend on your input settings (defined in your .inputrc file if present). By default, readline uses the emacs bindings such as C-a and C-e for the beginning and end of a line, M-b and M-f to move backward and forward by word, and so forth. We can navigate the command history using C-p (previous) and C-n (next) and search the history with C-r.
A second module, (ice-9 colorized), adds colors to Guile's shell. That's not as critical, but adds a nice touch:
(use-modules (ice-9 readline))
(activate-readline)
(use-modules (ice-9 colorized))
(activate-colorized)You may have to install the guile-readline and guile-colorized packages if Guile does not include them by default on your system. If you are using emacs, I recommend using the geiser package which comes with Guile support and its own REPL buffer mode (with keyboard navigation and history).
First Steps
With this configuration out of the way, let's start the Guile REPL and evaluate a few expressions:
$ guile
GNU Guile 3.0.11
Copyright (C) 1995-2024 Free Software Foundation, Inc.
Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'.
This program is free software, and you are welcome to redistribute it
under certain conditions; type `,show c' for details.
Enter `,help' for help.
scheme@(guile-user)> "Hello world!"
$1 = "Hello world!"
scheme@(guile-user)> $1
$2 = "Hello world!"
scheme@(guile-user)> 10
$3 = 10
scheme@(guile-user)> (+ 3 4)
$4 = 7
scheme@(guile-user)> (+ 2 3 (* 4 5))
$5 = 25We first enter the string "Hello world!". As in most programming languages, text has to be enclosed in double quotes to distinguish it from other program elements such as identifiers. Guile answers by telling us that the variable $1 now has the value "Hello world!". We verify this by entering this variable name $1 and get the same string back, this time as the value of the variable $2. A number such as 10 is echoed the same way.
Note that the $ prefix of the variable names is just the convention that the Guile REPL uses for the output variables. Scheme variable names can contain and start with any character unless it causes ambiguity such a digit or a quote as the first character.
The next expression, (+ 3 4) , already demonstrates most aspects of Scheme's syntax. A Scheme expression is either an "atom" or a list. An atom is a literal value such as a string ("Hello world!")~ or number (10), or a variable ($1). A list consists of a sequence of whitespace-separated expressions enclosed in parentheses.
As we have seen, literal values evaluate to themselves and a variable to the value that it is bound to. A list is evaluated by applying its first expression (in the example the + operator) to the following expressions (in the example the numbers 3 and 4).
The second arithmetic example, (+ 2 3 (* 4 5)), shows how this applies to nested expressions. The third argument of + is the multiplication (* 4 5) which evaluates to 20 so that the expression becomes (+ 2 3 20) resulting in 25.
S-Expressions
An expression using this syntax is known as s-expression (short for "symbolic expression"). The list s-expressions are called "forms". A Scheme program consists of forms that are evaluated according to the rule described above. In most cases, the first expression of a form evaluates to a function, and this function is applied to the following expressions (the function's arguments). Some "special" forms evaluate the following expressions in different ways, for example, by evaluating them conditionally, or performing some side effect on the environment such as defining a variable.
scheme@(guile-user)> (define x 100)
scheme@(guile-user)> (+ x 50)
$1 = 150
scheme@(guile-user)> (if (> x 50) "greater than 50" "less than or equal to 50")
$2 = "greater than 50"
scheme@(guile-user)> (set! x 25)
scheme@(guile-user)> (if (> x 50) "greater than 50" "less than or equal to 50")
$3 = "less than or equal to 50"We will look into both constructs in more detail later.
For arithmetic expressions, we have to use parentheses where we could otherwise rely on operator associativity and precedence. A coding environment that recognizes matching parentheses is definitely useful. On the plus side, operators often support an arbitrary number of arguments (including zero arguments).
scheme@(guile-user)> (+)
$1 = 0
scheme@(guile-user)> (*)
$2 = 1
scheme@(guile-user)> (* 2 3 4)
$3 = 24Another example is equality. Scheme forces us to be explicit about how we want to compare objects. The eq? function compares identity (basically the memory address) whereas eqv? and = compare numbers, and equal? performs a deep (and therefore costly) comparison of objects.
scheme@(guile-user)> (define a '(1 2 3))
scheme@(guile-user)> (define b '(1 2 3))
scheme@(guile-user)> (eq? a b)
$1 = #f
scheme@(guile-user)> (eq? a a)
$2 = #t
scheme@(guile-user)> (equal? a b)
$3 = #t
scheme@(guile-user)> (eqv? 10 (+ 5 5))
$5 = #t
scheme@(guile-user)> (= 10 (+ 5 5))
$6 = #tIn Guile, eq? works for numbers in many cases, but one should always use eqv? or = because the Scheme standard does not prescribe how numbers are stored.
Strings have their own set of comparison functions including case-insensitive (ci) versions.
scheme@(guile-user)> (string= "hello" "hello")
$1 = #t
scheme@(guile-user)> (string= "hello" "Hello")
$2 = #f
scheme@(guile-user)> (string-ci= "hello" "Hello")
$3 = #t
scheme@(guile-user)> (eq? (string-copy "hello") (string-copy "hello"))
$4 = #f
scheme@(guile-user)> (string= (string-copy "hello") (string-copy "hello"))
$5 = #tThe last two examples use string-copy to make sure that the strings live in different places in memory (otherwise eq? would return true).
Syntax in the time of AI coding
Witnessing the AI software development revolution, I'm wondering why we, the software engineering community, have spent so much time and effort on syntax. Every major LLM happily produces output in any syntax and often performs better for simpler and more regular syntax.
Why, for example, do we think it's important to match the syntax and precedence rules of conventional arithmetic expressions? From what I can tell, LLMs don't mind thinking in ASTs and happlily write as many parentheses as needed. On the other hand, even for humans I doubt that focusing on these somewhat arbitrary rules for months in elementary school helps to spread the love of math.
The conventional mathematical notation is not the outcome of a concerted effort to find the clearest expression of mathematical concepts but the result of centuries of trial and error, technical limitations (no LaTeX or computers), personal preferences, power struggles, and similar origins of "traditions". Just like natural languages (albeit to a lesser extent), mathematical notation suffers from ambiguity, context-dependence, and unnecessary complexity. A significant part of studying math and physics is devoted to getting used to this notation.
I'm sure that an AI that knows all the mathematical notations and all the natural and programming languages can come up with something better to represent complex structures on a flat surface. Maybe in a couple of years, our artificial overlords (or willing interlectual superpowers if we play it right) will give us a consistent syntax that is much better for students, engineers, scientists, and AI agents alike. It's a shame that Ken Iverson won't be around to witness this.
While we are waiting for this moment to arrive, we can enjoy the consistency of one of the oldest program notations. It still works, it's still surprisingly concise and flexible at the same time, not to mention its unmatched ability to manipulate itself (the homoiconicity enabling macros). There are no precedence rules and no separators (other than whitespace) to worry about, just a simple rule that the first element of a form determines how the form is evaluated.
A bit more Syntax
Scheme's syntax mostly consists of s-expressions, but there are a few more elements to learn.
Comments
A semicolon (outside of a string literal) indicates a comment and causes the rest of the line to be ignored. By convention, multiple semicolons are used for comments of increasing scope. A single semicolon is reserved (by convention) for comments to the rigth of some code. Comments with more than one semicolon start at the beginning of a line. File comments use four semicolons, section comments three, and comments for top-level entities such as functions use two.
;;;; A module with common math functions.
(define-module (example math)
#:export (fact))
;;; Simple math functions.
;; Returns the factorial of n.
(define (fact n)
(let loop ((n n) (product 1))
(if (zero? n)
product
(loop (- n 1) (* n product))))) ; tail-recursionI will not include comments in most examples because the accompanying text provides the description, but normally add comments liberally to explain the purpose of the code and any non-obvious logic.
Hash Syntax
Identifiers starting with a hash (#) are reserved for system-defined entities such as #t and #f for true and false, characters (for example, #\a for the letter "a"), and keyword symbols which start with #: (in contrast to Lisp's : prefix).
The hash syntax is also used for vector literals as we will see later.
Lists
Not surprising for a Lisp (list processing) language, lists are the built-in data structure in Scheme. If we want to use a list without evaluating it, we need to "quote" it by placing a single quote in front of the list.
scheme@(guile-user)> '(1 "foo" 3.5)
$1 = (1 "foo" 3.5)
scheme@(guile-user)> (quote (1 "foo" 3.5))
$2 = (1 "foo" 3.5)As the second expression shows, the ' syntax is just a shortcut for the quote special form. Without the quote, we get an error because Scheme doesn't know how to evaluate a form starting with a number.
scheme@(guile-user)> (1 "foo" 3.5)
ice-9/boot-9.scm:1705:22: In procedure raise-exception:
Wrong type to apply: 1
Entering a new prompt. Type `,bt' for a backtrace or `,q' to continue.
scheme@(guile-user) [1]> ,q
scheme@(guile-user)> Lists are implemented as singly-linked lists. A list is either empty () or a pair consisting of a value (the list's head) and another list (the list's tail). The cons (for "construct") function creates a new list from given head and tail.
scheme@(guile-user)> (cons 1 '(2 3))
$1 = (1 2 3)
scheme@(guile-user)> (cons 1 '())
$2 = (1)Dotted Pairs
When the second value is not a list, the pair is represented as a so-called "dotted pair", for example ("a" . 100).
scheme@(guile-user)> (cons "a" 100)
$1 = ("a" . 100)
scheme@(guile-user)> '("a" . 100)
$2 = ("a" . 100)
scheme@(guile-user)> '(1 . (2 . (3 . ())))
$3 = (1 2 3)To me, the dotted-pair syntax is an outlier. The dot looks like an infix operator while everything else uses prefix notation. In the end, the syntax is just a shortcut for the cons form.
We can get the first element of a pair with the car function and the second element with the cdr function (see their origin if you are curious). If the pair happens to be a list, the cdr function returns the tail of the list.
scheme@(guile-user)> '("a" . 100)
$1 = ("a" . 100)
scheme@(guile-user)> (car '("a" . 100))
$2 = "a"
scheme@(guile-user)> (cdr '("a" . 100))
$3 = 100
scheme@(guile-user)> (car '(1 2 3))
$4 = 1
scheme@(guile-user)> (cdr '(1 2 3))
$5 = (2 3)Besides car and cdr, Scheme supports most operations on lists you can imagine. Most functions return a new list, but one can also modify lists in place. Scheme is not a pure functional language.
Identifier naming conventions
By pure convention, identifiers of predicates (functions returning a boolean result) end with a question mark (for example, eq?) and functions mutating at least one of their arguments with an exclamation point (for example, set!). Conversions often use an arrow as in number->string for converting a number to its string representation.
scheme@(guile-user)> (define x 100)
scheme@(guile-user)> (integer? x)
$1 = #t
scheme@(guile-user)> (set! x 200)
scheme@(guile-user)> (number->string x)
$2 = "200"Symbols, Modules, and Variables
Local variables (lexical binding)
The let (special) form creates a scope with zero or more variables bound to some initial values.
scheme@(guile-user)> (let ((x 2)
(y 5))
(format #t "~a + ~a = ~a~%" x y (+ x y))
(set! x 3)
(+ x y))
2 + 5 = 7
$1 = 8Every variable declaration is a pair containing the variable's name and initial value such as (x 2), and all variable declarations are enclosed in another pair of parentheses. This declaration block is follow by one or more expressions (the body of the let form) that can use these variables. The value of the let expression is the value of the last expression in the body.
Nesting let forms creates nested scopes.
scheme@(guile-user)> (let ((x 2))
(format #t "x = ~a~%" x)
(let ((y (* x 10)))
(format #t "y = ~a~%" y)
(* x y)))
x = 2
y = 20
$1 = 40A let binding cannot reference a prior binding. One either has to use nested let forms as shown above or use the let* form:
scheme@(guile-user)> (let ((x 2)
(y (+ x 10)))
(* x y))
;;; :66:18: warning: possibly unbound variable `x'
ice-9/boot-9.scm:1705:22: In procedure raise-exception:
Unbound variable: x
scheme@(guile-user)> (let* ((x 2)
(y (+ x 10)))
(* x y))
$1 = 24Symbols
We have used identifiers such as cons and cdr above to reference built-in functions and defined our own variables such as x and y with define and let. Most programming languages treat such identifiers as something special that only the language environment can manipulate. In Scheme (and any other Lisp), these so-called symbols are objects in their own right that are accessible to and can be manipulated by programs.
When used without a quote, a symbol is evaluated to the value it is bound to or raises an error if the symbol is unbound. A quoted symbol is the symbol itself (similar to list quoting).
scheme@(guile-user)> foo
;;; :11:0: warning: possibly unbound variable `foo'
ice-9/boot-9.scm:1705:22: In procedure raise-exception:
Unbound variable: foo
scheme@(guile-user) [1]> ,q
scheme@(guile-user)> 'foo
$1 = foo
scheme@(guile-user)> (define foo 100)
scheme@(guile-user)> foo
$2 = 100
scheme@(guile-user)> (number? foo)
$3 = #t
scheme@(guile-user)> (symbol? foo)
$4 = #f
scheme@(guile-user)> (symbol? 'foo)
$5 = #tA Scheme symbol is an interned string. It has an identity and can be compare with eq?. We can convert back and forth between strings and symbols using the string->symbol and symbol->string functions. Scheme symbols are case sensitive.
scheme@(guile-user)> (eq? 'foo 'foo)
$1 = #t
scheme@(guile-user)> (eq? 'foo 'Foo)
$2 = #f
scheme@(guile-user)> (define x 'foo)
scheme@(guile-user)> (define y 'foo)
scheme@(guile-user)> (eq? x y)
$3 = #t
scheme@(guile-user)> (string->symbol "foo")
$4 = foo
scheme@(guile-user)> (eq? 'foo (string->symbol "foo"))
$5 = #t
scheme@(guile-user)> (symbol->string 'foo)
$6 = "foo"Modules and global variables
You may have been wondering what the (guile-user) output in the REPL prompt scheme@(guile-user)> refers to. It is the name of the module we are in. By default, a Guile REPL starts in the (guile-user) module. We can explicitly ask for it using the current-module function:
scheme@(guile-user)> (current-module)
$1 = #Modules comprise Scheme's packaging system. Modules bind symbols to variables. Conceptually, a module contains a map from symbols to variables. A variable may (and usually does) contain a value. Each module has a unique name which is a list of symbols such as (guile-user) or (ice-9 readline). The built-in Scheme functions such as cons live in the (guile) module.
The resolve-module function returns the module object for a given module name, and the module-variable function returns the variable that a symbol is bound to in a module.
scheme@(guile-user)> (module-variable (resolve-module '(guile)) 'cons)
$1 = #>
scheme@(guile-user)> (define foo 100)
scheme@(guile-user)> (module-variable (current-module) 'foo)
$2 = #
scheme@(guile-user)> (module-variable (current-module) 'bar)
$3 = #fIn contrast to Common Lisp, a Scheme identifier can be bound to only one value (which may be a function). It always struck me as odd that a language which is supposed to treat functions and other values on equal footing would store them in different cells.
Due to the long history of Scheme and Guile, there are multiple families of modules. Besides the Guile base modules such as guile and guile-user, module names typically start with a prefix such as srfi or ice-9 as in (srfi srfi-64) or (ice-9 match). The prefix indicates where the API comes from. Guile's original standard library uses ice-9. The srfi prefix stands for "Scheme Request for Implementation" which is the community-driven collection of APIs specified under https://srfi.schemers.org. The r6rs and r7rs prefixes stand for revision 6 and 7, R6RS and R7RS, of the Scheme standard. Adding to the confusion, R7RS consists of two specifications, a small one in the spirit of the original minimalist Scheme approach and a large one for a more practical "batteries included" language. Scheme implementations such as Guile typically support most parts of R6RS and R7RS as well as many of the SRFIs but often favor or at least preserve some of their own older modules. Guile's records and modules are two examples of that.
As we have already seen, the use-modules form imports the symbols of modules into the current module. It also allows us to import symbols selectively or to define a custom prefix for all imported symbols. In most cases I recommend to take advantage of selective imports and/or prefixes so that it is easy to see where a symbol comes from.
The define-module form defines a module's name and exported and imported symbols. It is placed at the beginning of the module's file and applies to all forms following the module declaration in the file. Here is the example of the "Hello world" web server from above defined as its own module:
(define-module (example hello-server)
#:export (start-hello-server)
#:use-module ((web server) #:select (run-server)))
(define (hello-world-handler request request-body)
(values '((content-type . (text/plain)))
"Hello World!\n"))
(define (start-hello-server)
(display "starting hello server\n")
(run-server hello-world-handler))The first argument of the define-module is the name of the module (example hello-server) as an unquoted list. The rest of the module definition consists of keywords arguments. The #:export keyword is followed by the (also unquoted) list of symbols that the module exports, here the single symbol start-hello-server. Those are the symbols that other module can import with use-modules. As an alternative to use-modules, the #:use-module keyword allows to import a single module. The keyword is followed by an interface specification. This can be just the module name or a list containing the module name and additional keyword arguments that specify how the imported module is used. The #:select keyword allows to import just the symbols needed by the importing module. In the example, we only need the run-server function from Guile's (web server) module.
If this file is stored in example/hello-server.scm, we can run the server with the following commands.
$ guile -L .
scheme@(guile-user)> (use-modules (example hello-server))
...
scheme@(guile-user)> (start-hello-server)
starting hello serverNote that we have to tell Guile with the -L . option where the module can be found if it is not installed in one of the standard locations. The Guile runtime resolves the module name by translating it to a directory path, the last part being the module's file (with .scm suffix for the source code or .go suffix for the compiled version).
As an alternative to #:select, we can define a prefix for an imported module. Using this technique, the hello-server module would look like this:
(define-module (example hello-server)
#:export (start-hello-server)
#:use-module ((web server) #:prefix ws:))
(define (hello-world-handler request request-body)
(values '((content-type . (text/plain)))
"Hello World!\n"))
(define (start-hello-server)
(display "starting hello server\n")
(ws:run-server hello-world-handler))We won't define custom modules for the examples below, but I recommend creating them early when starting a real project and adopting one of the explicit import styles with #:select or #:prefix to make the origin of each symbol obvious.
Unit tests
Before we move any further, let's introduce the SRFI 64 unit test module that we'll use to demonstrate Guile's features in a concise and reproducible fashion.
Here is a minimal example:
(use-modules (srfi srfi-64))
(test-begin "addition")
(test-eqv 7 (+ 3 4))
(test-end "addition")An SRFI 64 starts with test-begin and ends with test-end. Within this block, we can use the test- forms to verify our expectations. test-eqv compares an expected value (here 7) against the result of the tested expression (here (+ 3 4)).
Assuming we place this in a file called addition-test.scm, we can run the test as follows:
$ guile addition-test.scm
...
*** Entering test group: addition ***
* PASS:
*** Leaving test group: addition ***
*** Test suite finished. ***
*** # of expected passes : 1If we change the expected value from 7 to 6, the test runner shows the expression being evaluated as well as the expected and actual values.
$ guile addition-test.scm
*** Entering test group: addition ***
* FAIL:
source-file: .../addition-test.scm
source-line: 4
source-form: (test-eqv 6 (+ 3 4))
expected-value: 6
expected-error: #f
actual-value: 7
actual-error: #f
*** Leaving test group: addition ***
*** Test suite finished. ***
*** # of unexpected failures: 1The tests can be organized in a hierarchy using the test-group form. Here is a slightly larger example demonstrating Guile's string functions.
(use-modules (srfi srfi-64))
(test-begin "string tests")
(test-group "basics"
(test-group "predicates"
(test-assert (string? "foo"))
(test-assert (not (string? 123)))
(test-assert (string-null? ""))
(test-assert (not (string-null? "a"))))
(test-group "length"
(test-eqv 0 (string-length ""))
(test-eqv 3 (string-length "foo")))
(test-group "selection"
(test-eqv #\f (string-ref "foo" 0))))
(test-group "searching"
(test-group "suffix"
(test-assert (string-suffix? "oo" "foo"))
(test-assert (not (string-suffix? "a" "bar"))))
(test-group "prefix"
(test-assert (string-prefix? "fo" "foo"))
(test-eqv 2 (string-prefix-length "fo" "foo"))
(test-eqv 1 (string-prefix-length "fa" "foo"))))
(test-group "substrings"
(test-equal "ell" (substring "hello" 1 4)))
(test-group "join"
(test-equal "foo, bar, baz" (string-join '("foo" "bar" "baz") ", ")))
(test-end "string tests")We won't use the test groups in the small examples below, but they are useful when testing larger modules.
In the following examples, we will omit the SRFI-64 boilerplate (the use-modules, test-begin, and test-end forms).
Control expressions
In contrast to most procedural languages, all Scheme forms are expressions, including all forms controlling the flow of a program. They result in values and can be combined with any other expression. This makes Scheme programs compact and easy to follow (once one gets used to the parentheses). Variables, and in particular mutable variables, are needed less often. Most newer programming languages have adopted an expression-oriented syntax for this reason.
If-expression
The if expression, for example, takes the condition, then-expression, and else-expression as arguments. Depending on the value of the condition, the value of the if-expression is the value of the then-expression of the else-expression. If the else-expression is not provided, the result in the negative case is the special "unspecified" sentinel value which we can test with the unspecified? predicate.
(test-eqv 8 (if (= 5 (+ 2 3)) 8 9))
(test-eqv 9 (if (= 5 (+ 2 4)) 8 9))
(test-assert (unspecified? (if (= 5 6) 8)))
A dynamically-typed language can choose which values it considers "truthy" or "falsy" for conditions and logical operations. Python and JavaScript, for example, consider "empty" objects and zero falsy, and Lisp has the empty list (nil) as its false value.
>>> 10 if "" else 20
20
>>> 10 if 0 else 20
20
>>> 10 if [] else 20
20
>>> 10 if 1 else 20
10Scheme chose a single "falsy" value, namely #f. Every other value including the empty list '(), the empty string "", 0, and even unspecified is considered "truthy". Null-checks have to be explicit.
(test-eqv 10 (if "" 10 20))
(test-eqv 20 (if (not (string-null? "")) 10 20))
(test-eqv 10 (if 0 10 20))
(test-eqv 20 (if (not (zero? 0)) 10 20))
(test-eqv 10 (if '() 10 20))
(test-eqv 20 (if (not (null? '())) 10 20))
(test-eqv 20 (if #f 10 20))
(test-assert (unspecified? (if #f 1)))
(test-eqv 10 (if (if #f 1) 10 20))
Sometimes it's clearer to use the when or unless forms that take a condition and a single expression with the obvious meaning.
(test-eqv 10 (when (= 1 1) 10))
(test-assert (unspecified? (when (= 1 2) 10)))
(test-eqv 10 (unless (= 1 2) 10))
(test-assert (unspecified? (unless (= 1 1) 10)))
To achieve if-then-else-if logic, one can nest if-expressions. For logic with more conditions, the cond form is the clearer choice. It takes a list of (condition result) lists.
(define (f x)
(cond ((= x 10) 100)
((= x 20) 50)
(#t (+ 10 x))))
(test-eqv 100 (f 10))
(test-eqv 50 (f 20))
(test-eqv 15 (f 5))
For obvious performance reason, Scheme evaluates the "then" and "else" expressions only when needed (depending on the value of the condition). This only changes the behavior if the expression happen to have side effects.
(let ((result (if (> 3 2)
(begin (display "then-branch\n") 10)
(begin (display "else-branch\n" 20)))))
(test-eqv 10 result))
The begin expression evaluates a sequence of expressions and returns the result of the last one. This example only prints "then-branch" because the second begin expression is never evaluated. Lazy evaluation also applies to and and or expressions.
(test-eqv 20 (or (begin (display "or-branch-1\n") #f)
(begin (display "or-branch-2\n") 20)))
(test-eqv 10 (or (begin (display "or-branch-1\n") 10)
(begin (display "or-branch-2\n") 20)))
(test-assert (not (and (begin (display "and-branch-1\n") #f)
(begin (display "and-branch-2\n") 20))))
(test-eqv 20 (and (begin (display "and-branch-1\n") 10)
(begin (display "and-branch-2\n") 20)))
The output of this test is
or-branch-1
or-branch-2
or-branch-1
and-branch-1
and-branch-1
and-branch-2Do-expression
The do form is the universal loop construct (besides recursion) in Scheme. It controls one or more variables in a loop until a step condition is satified. The resulting final value (the do form is an expression!) is defined with another expression that typically depends on the variables of the loop.
Each variable starts with an arbitrary initial value and is update using an arbitrary expression. In the simplest case, one can let a variable loop through a sequence of integers by starting at some number and using the 1+ function to update the variable.
Syntactically, the form contains a list of variable definitions with variable name, initial value, and update expression. This variable form is followed by another list containing the stop and return expressions. The remaining forms present the body that is being executed for every iteration of the loop.
(let ((result (do ((i 0 (1+ i))
(j 1 (+ j 2)))
((> (+ i j) 10) (* i j))
(format #t "i=~a j=~a~%" i j))))
(test-eqv 36 result))
The loop counts i from 0 by 1 and j from 1 by 2. It stops when their sum becomes greater than 10 (which happens at i=4 and j=9) and returns their product 4*9=36 as the result of the loop. The output look as follows:
i=0 j=1
i=1 j=3
i=2 j=5
i=3 j=7Recursive loops (named let)
This combination of flexible initialization, update, break condition, and result expression covers a lot of loop patterns.
However the at least as flexible and more idiomatic way of looping is recursion using the named let. Here is the same loop using a named let form.
(let ((result (let loop ((i 0) (j 1))
(if (> (+ i j) 10)
(* i j)
(loop (1+ i) (+ 2 j))))))
(test-eqv 36 result))
The identifier after let allows us to "call" the let expression recursively with different values from within the let form. In other words, the let bindings become arguments of a function with the given name, here loop, that we can call from the body of the let expression. In this example, we return the product i*j if the condition is met and otherwise call the loop recursively with the updated values. The name of the named let can be any identifier, and loop is just a common one.
Scheme guarantees that this loop is as efficient as a "normal" loop if the recursive call is in "tail position", that is, the last executed expression. If that's the case, there won't be any nested function call, and we don't have to worry about stack overflow.
Recursion may initially look foreign but quickly turns out to be the most direct and easiest-to-reason-about way to implement many iterative algorithms.
Functions
The define form binds a symbol to a value:
(define x 10)
(test-eqv 10 x)
A function value is constructed with a lambda form.
(define f (lambda (a b) (+ a b 10)))
(test-eqv 15 (f 2 3))
Instead of binding to the function value to the variable f we could have evaluated it directly.
(test-eqv 15 ((lambda (a b) (+ a b 10)) 2 3))
This is not useful in this particular case, but it shows that we can treat a function value like any other value.
Functions are obviously the bread and butter of a functional language such as Scheme so that there is a shortcut for function definitions that follows the usage pattern:
(define (f a b)
(+ a b 10))
(test-eqv 15 (f 2 3))
Here is a slightly more realistic example with proper descriptive function and argument names:
(define (compute-interest principal rate-percent n)
(- (* principal (expt (1+ (/ rate-percent 100.0)) n)) principal))
(test-approximate "interest" 7.88 (compute-interest 50 5 3) 0.01)
Being a functional language, Scheme allows for returning functions just like any other value.
(define (add x) (lambda (y) (+ x y)))
(define a5 (add 5))
(test-eqv 7 (a5 2))
(test-eqv 15 (a5 10))
This can be viewed as a special case of binding a variable to a function using two lambdas.
(define add (lambda (x) (lambda (y) (+ x y))))
(define a5 (add 5))
(test-eqv 7 (a5 2))
(test-eqv 15 (a5 10))
Similar to the function definition syntax, there is a shortcut for these function-returning function definitions. It used to be enabled by default, but now requires an import of the (ice-9 curried-definitions) module.
(use-modules (ice-9 curried-definitions))
(define ((add x) y) (+ x y))
(define a5 (add 5))
(test-eqv 7 (a5 2))
(test-eqv 15 (a5 10))
This is particularly useful for functions manipulating other functions (higher-order functions) such as the following compose function that applies one function after another.
(use-modules (ice-9 curried-definitions))
(define ((compose f g) x) (f (g x)))
(define (f x) (+ 10 x))
(define (g x) (* 3 x))
(test-eqv 25 ((compose f g) 5))
(test-eqv 45 ((compose g f) 5))
Variable arguments: optional, keyword, and remaining arguments
Functions can accept a variable number of arguments. This is indicated by a dot . followed by the name of the variable that will contain the remaining arguments as a list. The following function adds the product of the remaining args to the sum of the first two arguments (where the empty product is 1 as we have seen above).
(define (f a b . args) (+ a b (apply * args)))
(test-eqv 6 (f 2 3))
(test-eqv 25 (f 2 3 4 5))
Adding a * suffix to define (an * often indicates some "special" version of a form) let's us define functions with optional arguments (via #:optional), keyword arguments (via #:key), and variable argument lists (via #:rest). The #:rest args syntax is equivalent to the . args syntax in the previous example.
(define* (f a b #:rest args) (+ a b (apply * args)))
(test-eqv 6 (f 2 3))
(test-eqv 25 (f 2 3 4 5))
An optional argument (following the #:optional keyword in the argument list) is specified either as a list (non-dotted pair) containing the argument name and the default value or as just the argument name in which case the default value is #f. Keyword arguments use the same pair syntax following the #:key keyword.
(define* (f x #:optional (y 1) (z 0)) (+ (* x y) z))
(define* (g x #:key (y 1) (z 0)) (f x y z))
(test-eqv 10 (f 10))
(test-eqv 20 (f 10 2))
(test-eqv 25 (f 10 2 5))
(test-eqv 10 (g 10))
(test-eqv 20 (g 10 #:y 2))
(test-eqv 25 (g 10 #:y 2 #:z 5))
We can combine keyword and remaining arguments. One twist is that the rest arguments include the keyword arguments. To handle this, let's define a function that removes the keyword arguments.
(define (remove-keyword-args args)
(let loop ((args args) (result '()))
(if (null? args)
(reverse result)
(if (keyword? (car args))
(loop (cddr args) result)
(loop (cdr args) (cons (car args) result))))))
(test-equal '(1 2 3) (remove-keyword-args '(1 2 3)))
(test-equal '(1 3) (remove-keyword-args '(1 #:foo 2 3)))
(test-equal '(1 3 5) (remove-keyword-args '(1 #:a 2 3 #:b 4 5)))
(define* (f a b #:optional (c 0) #:key (d 0) #:rest args)
(+ a b c d (apply * (remove-keyword-args args))))
(test-eqv 7 (f 1 2 #:d 3))
(test-eqv 9 (f 1 2 5))
(test-eqv 11 (f 1 2 3 #:d 4))
(test-eqv 40 (f 1 2 3 #:d 4 5 6))
Scheme normally rejects keyword arguments that have not been specified with #:key. If we want more flexibility and accept other keys as part of the #:rest argument, we can turn this check off with #:allow-other-keys. To test this, we are defining another helper function that looks for the value of a keyword argument in an argument list.
(define (keyword-arg args keyword)
(let loop ((args args))
(if (null? args)
#f
(if (keyword? (car args))
(if (eq? (car args) keyword)
(cadr args)
(loop (cddr args)))
(loop (cdr args))))))(test-eqv 10 (keyword-arg '(#:a 10 1 2) #:a))
(test-eqv 10 (keyword-arg '(1 #:b 20 #:a 10) #:a))
With this helper function, we can handle arbitrary keyword arguments.
(define* (f #:key (a 10) #:allow-other-keys #:rest args)
(let ((b (or (keyword-arg args #:b) 20))
(c (or (keyword-arg args #:c) 30)))
(+ a b c)))
(test-eqv 60 (f))
(test-eqv 51 (f #:a 1))
(test-eqv 6 (f #:a 1 #:b 2 #:c 3))
While we can do almost anything with a function's arguments, it's typically clearer to stick to normal positional arguments, possibly adding keyword arguments for long argument lists if needed.
Multiple return values
A function normally returns a single value, the result of evaluating the last expression of the function's body. This result may be a composed value such as a list which allows us to return multiple values to the caller. Sometimes we want return multiple values but leave it up to the caller how many of these return values to use. The first return value is often the main result whereas the following return values provide optional information.
As an example, the floor/ function performs division with remainer and returns the (integer) quotient as the first value and the remainder as the second. When we call floor/ from the REPL, we can see both return values as separate REPL variables ($1 and $2).
scheme@(guile-user)> (floor/ 10 3)
$1 = 3
$2 = 1
scheme@(guile-user)> (floor-quotient 10 3)
$3 = 3
scheme@(guile-user)> (floor-remainder 10 3)
$4 = 1
scheme@(guile-user)> (+ (floor/ 10 3) 10)
$5 = 13
scheme@(guile-user)> (let ((q (floor/ 10 3))) (display q) (newline))
3The last two expression show that we can use the first return value q without doing anything special. To access more than the first value, we can use the receive form defined in the (ice-9 receive) module (and SRFI-8).
(use-modules (ice-9 receive))
(receive (q r) (floor/ 10 3)
(test-eqv 3 q)
(test-eqv 1 r))
Alternatively, we can use the let-values form defines in SRFI-11 which let's use combine normal and multi-value bindings.
(use-modules (srfi srfi-11))
(let-values (((q r) (floor/ 10 3))
((x) 10))
(test-eqv 3 q)
(test-eqv 1 r)
(test-eqv 10 x))
We can also pass the values to another function with the call-with-values form. It takes a (argument-less) function returning the multiple values as the first argument and another function accepting these values as arguments.
(test-eqv 14 (call-with-values (lambda () (floor/ 10 3))
(lambda (q r) (+ q r 10))))
I don't know why this form requires a function producing the values rather than the values themselves. In most cases, it's clearer to use receive or let-values.
To produce multiple values ourselves, we pass them to the values form.
(use-modules (ice-9 receive))
(receive (a b) (values 10 20)
(test-eqv 10 a)
(test-eqv 20 b))
A function returning multiple values uses values as the last expression. The following function expands the keyword-arg function to also return whether the keyword was found or not. This allows for keyword arguments whose value is #f.
(use-modules (ice-9 receive))
(define (keyword-arg args keyword)
(let loop ((args args))
(if (null? args)
(values #f #f)
(if (keyword? (car args))
(if (eq? (car args) keyword)
(values (cadr args) #t)
(loop (cddr args)))
(loop (cdr args))))))
(receive (value found) (keyword-arg '(#:a 10) #:a)
(test-eqv 10 value)
(test-eq #t found))
(receive (value found) (keyword-arg '(#:a #f) #:a)
(test-eqv #f value)
(test-eq #t found))
More structures
Records
Almost every programming language has some concept of a "structure" that combines multiple field into a single entity. Scheme has many! Thanks to its macro system, Scheme makes it too easy to implement something that looks like structures or "records" to use Scheme's preferred term. There are Guile's own low-level records, SRFI-9 records, R6RS records, and the classes of Guile's object oriented subsystem called Goops. While syntax and some of the functional details differ, all these record systems are similar.
We are only going to look at SRFI-9 records because it's the record system recommended by the Guile reference manual. We will give a brief introduction to Goops in a separate section.
Let's start with a simple (mutable) record modeling a person's name:
(use-modules ((srfi srfi-9) :select (define-record-type)))
(define-record-type
(make-person-name first-name last-name)
person-name?
(first-name person-name-first-name set-person-name-first-name!)
(last-name person-name-last-name))
(let ((p (make-person-name "Homer" "Simpson")))
(test-assert (person-name? p))
(test-equal "Homer" (person-name-first-name p))
(test-equal "Simpson" (person-name-last-name p))
(set-person-name-first-name! p "Bart")
(test-equal "Bart" (person-name-first-name p)))
The define-record-type (obviously) defines a new record type. In the example, we call this type . Note that the angle brackets are not part of the syntax, but just a convention. Record type names often use angle brackets to avoid name clashes with other entities such as variables.
The type name is followed by the constructor expression. Again, the make- prefix is not mandatory, but a common convention. The next term of the record definition is the record type's predicate function. Scheme will automatically define a function with this name that can be used to check if a given value is of the declared record type.
The remaining terms define the fields of the record. Each field is describe with a list of field name, getter, and optional setter. In the example, we define a setter for the first name, but not the last name.
Guile comes with an extension that allows to define immutable record types. Here is an example of an immutable point type:
(use-modules (srfi srfi-9)
(srfi srfi-9 gnu))
(define-immutable-record-type
(make-point x y)
point?
(x point-x)
(y point-y))
(let ((p (make-point 10 20)))
(test-assert (point? p))
(test-eqv 10 (point-x p))
(test-eqv 20 (point-y p)))
The (srfi srfi-9 gnu) module contains, among other things, the define-immutable-record-type form that, as the name indicates, create an immutable record. Setters are not allowed, and lower-level ways to set the fields are also turned off.
Vectors
Homogeneous "flat" vectors are the most efficient data structure on modern CPUs (and even more so on GPUs), and recent Scheme dialects such as Guile have comprehensive support for vectors. Like lists, Guile's default vector structure is heterogeneous. Elements can be of different types. Small elements such as (small) integers can still be stored and accessed efficiently in one block of memory. Larger heap-allocated elements require one indirect addressing but are still much faster to access than elements in a list.
Similar to lists, vectors can be constructed with the vector form or the #(...) syntax which acts like the quote '(...) syntax for lists. The forms inside the #(...) are not evaluated. Elements can be referenced with vector-ref using zero-based indexing. Vectors constructed with vector are mutable and elements can be set with vector-set!. Vectors constructed with the #(...) syntax are immutable and throw an error when we attempt to modify them.
(let ((a (vector 1 "a" (+ 2 3)))
(b #(1 "a" (+ 2 3))))
(test-assert (vector? a))
(test-assert (vector? b))
(test-equal a #(1 "a" 5))
(test-eqv 3 (vector-length a))
(test-eqv 1 (vector-ref a 0))
(test-eqv 5 (vector-ref a 2))
(test-eqv '(+ 2 3) (vector-ref b 2))
(vector-set! a 1 "b")
(test-equal a #(1 "b" 5)))
Again similar to lists, there is a host of functions operating on vectors. Vectors can by copied, shifted, mapped, and constructed from formulas in various ways. Many of these vector functions are in the SRFI-43 module.
The make-vector function creates a vector filled with some value. The vector-unfold and vector-unfold-right functions compute the elements based on the index and zero or more seed values. The initialization callback takes the index and current seeds and returns the element and the new seeds.
Using values as the callback, the vector-unfold acts like a "range" function. The second example shows how to generate a Fibonacci sequence using two seed values.
(use-modules (srfi srfi-43))
(test-equal #("a" "a" "a") (make-vector 3 "a"))
(test-equal #(0 1 2 3) (vector-unfold values 4))
(test-equal #(1 1 2 3 5 8 13 21)
(vector-unfold (lambda (i a b) (values (+ a b) b (+ a b))) 8 1 0))
Unlike collection libraries of newer languages, the functions operating on the different Scheme collection types have specific names and slight differences. The vector functions taking element callbacks such as vector-map and vector-for-each functions, for example, pass the index and element to the callback function whereas the list functions such as map passes only the element to the callback.
(use-modules (srfi srfi-43))
(test-equal
#("1: a" "2: b" "3: c")
(vector-map (lambda (i elem) (format #f "~a: ~a" (1+ i) elem)) #("a" "b" "c")))
Note that format called with #f returns the formatted string instead of writing it to some output stream.
There are various functions to search for an element in a vector and return the associated index. These functions return #f if there is no matching element so that the return value can be checked in if-expressions or combined with or (for example, for fallbacks).
(use-modules (srfi srfi-43))
(test-eqv 3 (vector-index even? #(1 3 5 2 3)))
(test-assert (not (vector-index even? #(1 3 5 7 3))))
(test-eqv 3 (or (vector-index even? #(1 3 5 4 3)) -1))
(test-eqv -1 (or (vector-index even? #(1 3 5 7 3)) -1))
There is also a built-in binary-search function for sorted vectors.
(use-modules (srfi srfi-43))
(define (string-compare-to s1 s2)
(cond
((string<? s1 s2) -1)
((string>? s1 s2) 1)
(else 0)))
(test-eqv 2 (vector-binary-search #("bar" "baz" "foo" "qux") "foo" string-compare-to))
Besides the default heterogenenous vector, Guile Scheme supports specialized vector types for numeric elements and in particular byte vectors. Guile's byte vectors can be interpreted as vectors of fixed-size integers using the CPU's endianness which provides the most effecient way to store and process numeric data using Scheme and pass the data between Scheme and numerical libraries.
Similar to vectors and lists, byte vectors can be constructed with an explicit make-bytevector form or the special #vu8(...) syntax. The functions operating on byte vectors start with bytevector- (for example, bytevector-length or bytevector-fill). There is no single ref function because byte vectors can be interpreted as vectors of any fixed-size integer type that Scheme supports. The type indicators use short names such as u8, u16 for 8-bit and 16-bit unsigned integers or s32 for 32-bit signed integers (not i32 as in Rust).
The most common raw byte vector access uses bytevector-u8-ref and bytevector-u8-set!.
(use-modules (rnrs bytevectors))
(test-eq 'little (native-endianness))
(let ((bv #vu8(1 50 100)))
(test-eqv 1 (bytevector-u8-ref bv 0))
(test-eqv 100 (bytevector-u8-ref bv 2)))
Byte vectors can easily be converted to and from strings using some unicode encoding, typically UTF-8:
(use-modules (rnrs bytevectors))
(test-equal #vu8(102 111 111) (string->utf8 "foo"))
(test-equal "foo" (utf8->string #vu8(102 111 111)))
When interpreting the byte vector as a vector of multi-byte numbers, the caller has to perform the index arithmetic to compute the byte index and provide the endianness or use the -native variants of the accessor functions.
(use-modules (rnrs bytevectors))
(let ((bv (make-bytevector 8 0)))
(bytevector-s16-set! bv 2 92 (native-endianness))
(test-equal "#vu8(0 0 92 0 0 0 0 0)" (format #f "~a" bv))
(test-eqv 92 (bytevector-s16-ref bv 2 (native-endianness)))
(test-eqv (* 92 256) (bytevector-s16-ref bv 2 'big))
(bytevector-s16-native-set! bv 0 1024)
(test-equal "#vu8(0 4 92 0 0 0 0 0)" (format #f "~a" bv))
(test-eqv 1024 (bytevector-s16-native-ref bv 0)))
As one can imagine, using raw bytevectors as numeric arrays is quite error-prone. Scheme raises an error if the (byte) index is out of bounds, but apart from this, we are on our own.
Fortunately, Scheme also supports (dense) tensors of arbitrary rank that are stored in one-dimensional vectors (in row-major order). These (multi-dimensional) come in heterogeneous and typed versions. A typed array contains elements of a fix numeric element type. The examples focus on these typed arrays.
(let ((a (make-typed-array 'u32 0 2 3)))
(test-assert (typed-array? a 'u32))
(test-equal "#2u32((0 0 0) (0 0 0))" (format #f "~a" a))
(test-equal '((0 1) (0 2)) (array-shape a))
(array-set! a 1024 0 1)
(test-eqv 1024 (array-ref a 0 1)))
The array- functions are polymorphic. The typed array knows its element type, rank, and index ranges. We don't have to perform any index gymnastics.
Maps (association lists and hash table)
Maps, also known as dictionaries, associate keys with values. Scheme's original key-value data structure (inherited from Lisp) is the association list or alist for short. It is a standard (singly linked) list of key-value (dotted) pairs. The acons function prepends a new key-value pair to an alist and returns the new list.
(let ((a '()))
(set! a (acons "a" 100 a))
(set! a (acons "b" 200 a))
(set! a (acons "a" 300 a))
(test-equal '(("a" . 300) ("b" . 200) ("a" . 100)) a))
We have to assign the new alists back to the original variable to "update" the alist. As we can see, this process does not check if the new key already exists in the alist. It is most useful when duplicate keys are permitted or when we know that the keys are distinct.
To find a key, we need to decide which equality function to use. Scheme provides three different "assoc" functions performing the lookup with the three main equality functions: assoc uses equal?, assv uses eqv?, and assq uses eq?.
;; using equal? and assoc for arbitrary keys (strings, numbers, list)
(let ((a '(("a" . 100) ("b" . 200) (42 "foo") ("a" . 400) ((1 2) . "bar"))))
(test-equal '("a" . 100) (assoc "a" a))
(test-equal '(42 "foo") (assoc 42 a))
(test-equal '((1 2) . "bar") (assoc '(1 2) a))
(test-eq #f (assoc "c" a)))
;; using eqv? and assv? for numeric keys
(let ((a '((10 . "foo") (20 . "bar"))))
(test-equal '(10 . "foo") (assv 10 a))
(test-assert (not (assv 30 a))))
;; using eq? and assq? for identity types such as symbols
(let ((a '((x . 100) (#:y . 200))))
(test-equal '(x . 100) (assq 'x a))
(test-equal '(#:y . 200) (assq #:y a)))
Note that the assoc functions take the key as the first argument and the alist as the second argument. They return the dotted key-value pair if a matching key is found, not just the value, and false (#f) if the key is not found.
To avoid the duplicate keys and set the value if the key already exists, we can use the "assoc set!" functions. Similar to the lookup functions, there are three of them for the three different equality checks, assoc-set!, assv-set!, and assq-set!. These functions still return new alists when adding a new key so that we still have to reassign the return value. Somewhat inconsistently, these set functions take the alist as the first argument followed by the key and value.
(let ((a '()))
(set! a (assoc-set! a "a" 100))
(set! a (assoc-set! a "b" 200))
(set! a (assoc-set! a "a" 300))
(test-equal '(("b" . 200) ("a" . 300)) a)
(test-equal '("a" . 300) (assoc "a" a)))
The alist lookup complexity is proportional to the number of items. That's OK for small maps, but to achieve constant lookup time, we have to resort to hash tables. Internally, Scheme uses alists for the buckets of a hash table. The hash and equality functions are not kept in the hash table itself. Instead, there are different lookup (-ref), set (-set!), and remove (-remove!) functions for the different hash and equality options. The variations use the prefixes hash-, hashq-, hashv-, and hashx-. The hash- functions use equal?, the hashq- functions eq?, and the hashv- functions eqv?. These functions use the built-in hash functions to determine the key's bucket in the hash table. The hashx- functions use caller-provided functions to compute the bucket index and to look up a key in a bucket alist.
;; using hash-ref, hash-set! for arbitrary keys
(let ((h (make-hash-table)))
(test-assert (hash-table? h))
(hash-set! h "a" 100)
(hash-set! h "b" 200)
(test-equal 100 (hash-ref h "a"))
(test-assert (not (hash-ref h "c"))))
;; using hashv-ref, hashv-set! for numeric keys
(let ((h (make-hash-table)))
(hashv-set! h 10 "foo")
(hashv-set! h 20 "bar")
(test-equal "foo" (hashv-ref h 10))
(hash-clear! h)
(test-equal #f (hashv-ref h 10)))
;; using hashq-ref, hashq-set! for identity types such as symbols
(let ((h (make-hash-table)))
(hashq-set! h #:a "foo")
(hashq-set! h #:b "bar")
(test-equal "foo" (hashq-ref h #:a))
(hashq-remove! h #:a)
(test-equal #f (hashq-ref h #:a)))
The make-hash-table function takes an optional initial size (number of buckets). There are various functions operating on a hash table as a whole, for example, converting back and forth between lists and hash tables, mapping and folding (accumulating) the values in a hash table, or counting the entries satisfying a given predicate.
(let ((h (make-hash-table)))
(hash-set! h "a" 100)
(hash-set! h "b" 200)
(hash-set! h "c" 300)
(test-eqv 2 (hash-count (lambda (k v) (> v 100)) h))
(test-eqv 600 (hash-fold (lambda (k v acc) (+ acc v)) 0 h)))
The hash table functions described so far comprise Guile's original hash table implementation. There are two new APIs that make hash tables easier to use. SRFI-69 defines a hash table API that includes the equality and hash functions in the table itself (passed to the make-hash-table function). This allows for more consistent hash-table-ref, hash-table-set!, hash-table-delete!, and hash-table-exists? functions. Finally, R6RS is the hash table API of the Scheme standard.
(use-modules (rnrs hashtables))
(let ((h (make-eq-hashtable)))
(test-assert (hashtable? h))
(test-eqv 0 (hashtable-size h))
(hashtable-set! h 'a 100)
(hashtable-set! h 'b 200)
(test-eqv 100 (hashtable-ref h 'a #f))
(test-assert (hashtable-contains? h 'a))
(test-assert (not (hashtable-ref h 'c #f))))
While the original hash table API is the still most common, R6RS hash tables are gaining traction as the most portable alternative.
Numbers
Scheme supports integers of any size (only limited by memory), fractions (with numerators and denominators of any size), floating point "real" numbers, and (floating point) complex numbers. Scheme converts values up this "tower" of number types automatically, and numerical operators work as expected on all these types.
Integers and rationals
There is no integer overflow as in many languages with fixed-size (e.g., 32 bit or 64 bit) integers. Guile switches automatically between the CPU's integer representation and (still relatively efficient) custom structures for large integers.
scheme@(guile-user)> (expt 2 3)
$1 = 8
scheme@(guile-user)> (expt 2 100)
$2 = 1267650600228229401496703205376
scheme@(guile-user)> (define (fact n)
(let loop ((n n) (product 1))
(if (= n 0) product (loop (- n 1) (* product n)))))
scheme@(guile-user)> (fact 100)
$3 = 9332621544394415268169923885626670049071596826438162146859296389521
7599993229915608941463976156518286253697920827223758251185210916864000000
000000000000000000Dividing two integers a and b results in a rational number or an integer (if b divides a).
scheme@(guile-user)> (/ 10 2)
$1 = 5
scheme@(guile-user)> (/ 10 3)
$2 = 10/3
scheme@(guile-user)> (integer? (/ 10 2))
$2 = #t
scheme@(guile-user)> (integer? (/ 10 3))
$3 = #f
scheme@(guile-user)> (rational? (/ 10 3))
$4 = #t
scheme@(guile-user)> (/ (fact 100) (fact 99))
$5 = 100Rational numbers can also be defined as literals using the infix / (without spaces):
scheme@(guile-user)> (rational? 12/5)
$1 = #t
scheme@(guile-user)> (numerator 12/5)
$2 = 12
scheme@(guile-user)> (denominator 12/5)
$3 = 5
scheme@(guile-user)> (+ 2/3 3/4 4/5)
$4 = 133/60Real and complex numbers
An "inexact" real number is a floating point number. Because of the finite precision, it is always a rational number. This implies in particular that the result of (sqrt 2) is rational (but inexact) in Scheme.
scheme@(guile-user)> (sqrt 2)
$1 = 1.4142135623730951
scheme@(guile-user)> (rational? (sqrt 2))
$2 = #t
scheme@(guile-user)> (exact? (sqrt 2))
$3 = #f
scheme@(guile-user)> (exact? (sqrt 4))
$4 = #t
scheme@(guile-user)> (integer? (sqrt 4))
$5 = #tComplex numbers are defined with real and imaginary part using the mathematical notation such as 3+4i. The real part is mandatory, that is, the imaginary unit i is 0+i.
scheme@(guile-user)> (+ 3+4i 4+5i)
$1 = 7.0+9.0i
scheme@(guile-user)> (* 3+4i 4+5i)
$2 = -8.0+31.0i
scheme@(guile-user)> (expt 0+i 2)
$3 = -1.0+0.0iBit fiddling
Scheme supports the usual bitwise operations on integers (two-complement representation in case of negative numbers) such as "and", "or", "xor", and "not". They are called logand, logior (inclusive or), logxor, and lognot.
(test-eqv 4 (logand 5 14))
(test-eqv 15 (logior 5 14))
(test-eqv 11 (logxor 5 14))
(test-eqv -6 (lognot 5))
We can also test individual bits with logbit? and count them with logcount.
(test-eq #t (logbit? 0 5))
(test-eq #f (logbit? 1 5))
(test-eq #t (logbit? 2 5))
(test-eqv 2 (logcount 5))
(test-eqv 3 (logcount 14))
The SNFI-60 module defines more bit operations on integers such as bit-field and bitwise-if.
Input and output
To interact with the "real world", a program has to read and write data. On a Unix system, this means reading from and writing to files which includes network IO using sockets and other "pseudo files".
Guile's concept of an IO "channel" is a port. Opening a file creates a port, and Guile's IO operations act on this port. The API is flexible enough to allow for custom ports that read from or write to strings and byte vectors.
Here is the "Hello, world!" of file IO straight from the reference manual:
(let ((port (open-output-file "foo.txt")))
(display "Hello, world!\n" port)
(close-port port))The call-with-output-file shortcut ensures that the port is closed even in the case of exception:
(use-modules (ice-9 textual-ports))
(call-with-output-file "foo.txt"
(lambda (port)
(display "Hello, world!\n" port)))
(call-with-input-file "foo.txt"
(lambda (port)
(test-equal "Hello, world!\n" (get-string-all port))))
There is a wealth of functions for reading and writing byte vectors and text.
Macros
No Scheme overview is complete without showing its macro system(s). Let's start with the "legacy" macros inherited from Lisp. We have already discussed that a program consists of s-expressions and that Scheme is good at manipulating s-expressions.
A quoted list does not evaluate any of its arguments. If we use the backquote ` instead of the single quote ' in front of a list, we can turn off the quoting inside of the list with the comma , operator. We can also insert all items of a list using the comma-at ,@ operator. Together, these three operators define a templating system for s-expressions.
(let ((names '("Alice" "Bob"))
(number 42))
(test-equal '(number 42) `(number ,number))
(test-equal '(("Alice" "Bob")) `(,names))
(test-equal '(42 "Alice" "Bob") `(,number ,@names)))
Legacy macros
Scheme's legacy macro system inherited from Lisp uses the define-macro form. A macro call looks like a function call, but while a function is evaluated at runtime by evaluating the argument expressions and passing the resulting values as argument to the function, the macro is called at load time with the (unevaluated) argument expressions. The macro is evaluated and the result code (s-expressions) is inserted in place of the macro "call".
The hello-world of macros is the "unless" macro.
(define-macro (my-unless condition . body)
`(if (not ,condition)
(begin ,@body)))
(test-equal "foo" (my-unless (= 1 2)
(display "one is not two")
"foo"))
If we want to see the code generated by the macro, we can pass the expression to the macroexpand function. We also need to pipe it through tree-il->scheme to convert Guile's internal AST IL (abstract syntax tree intermediate language) to Scheme code.
(use-modules (language tree-il))
(test-equal
'(if (not (= 1 2)) (begin (display "one is not two") "foo"))
(tree-il->scheme (macroexpand '(my-unless (= 1 2) (display "one is not two") "foo"))))
Define syntax
Lisp's macros are extremely powerful but also quite dangerous. The code generated by the macro is "pasted" into the context of the macro call. The code may directly modify this context, override local variables, and so forth, without a clear indication at the call site. One can work around most of these issues through careful macro design (for example, using unique generated symbols), but this is not enforced by the language.
Scheme's own macro system avoids these problems. Here is the equivalent version of my-unless.
(define-syntax my-unless
(syntax-rules ()
((my-unless condition body1 body2 ...)
(if (not condition)
(begin body1 body2 ...)))))
(test-equal "foo" (my-unless (= 1 2)
(display "one is not two")
"foo"))
The define-syntax form defines a new macro with the given name, here my-unless. The name is followed by a so-called transformer. The most common one is syntax-rules which generates the code by pattern matching. For my-unless, we need only one pattern. An expression followed by ... in the template stands for zero or more occurrences. To enforce at least one body expression, we preceed this with a single body1 expression. Otherwise we could generate an empty begin expression leading to a runtime error. The pattern language support lists, vectors, records, and arbitrary nesting.
New Scheme macros are almost exclusively written with define-syntax, but there is still a lot of older code using define-macro. It is also useful to know the unquoting syntax because it is used outside of macros.
Continuations
Feel free to skip this section if you are not interested in asynchronous IO.
Many algorithms become easier when we can suspend a computation and a resume it later. The most common case is asynchronous IO where we initiate some IO operation and continue once the operation completes. While we are waiting, we want to use the thread to do other useful work such as processing other IO operations. Normal functions can only stop and wait for some event by blocking the current thread. The standard (Linux) IO functions block until the IO operation is complete. This does not scale well because of the significant overhead of each thread.
To work around this limitation, we can divide the function into multiple "callback" functions or identify the suspension points and model them as states in a state machine. Both solutions add a lot of complexity to what should be straightforward suspend-and-resume sequential logic.
Many language now offer support for "coroutines" as a solution, that is, functions that can be suspended and resumed later while the underlying thread is performing other work. This moves the burden of managing the state machine from the programmer to the compiler and language runtime.
Call-with-current-continuation
Standard Scheme allows us to capture the state of program at some expression in the form of a "continuation". These continuation objects can be stored and called like functions. When calling the continuation will some value, the program will reenter the program where the continuation was created with the value as the result of the expression.
The continuation is created with the call-with-current-continuation (or call/cc for short) form. It's only argument is the continuation handler function. Scheme creates the continuation and passes it to the handler. The handler can do with the continuation whatever it likes, for example, store it for later execution. When the handler returns, its return value is used as the result of the call/cc expression. When the continuation is called, its argument is used as the result of the call/cc expression. The continuation can be called by the handler itself or stored by the handler and then called later.
As you can tell, this can get convoluted very quickly. I won't even show an example because there are better options. One problem is the global nature of the continuation all the way up the call stack. It forces us to use global state and logic (almost like a Unix fork). What we really want is a delimited continuation up to some enclosing expression that confines the logic to a local scope.
Prompts
Guile's solution for delimited continuations is the prompt API. The metaphor is a command line prompt. A command line interpreter (CLI) such as a shell executes some logic until it hits a point where the user needs to enter data. The program shows the user a prompt and continues after the user has entered the data.
The call-with-prompt form creates the scope for the continuation logic. To be able to handle multiple nested scopes, the prompt is identified with a tag created with make-prompt-tag. Besides the tag, the call-with-prompt form takes the body to be executed as a body "thunk" (an argument-less function) and the handler of the prompt calls. When the body calls abort-to-prompt, the handler is called with delimited continuation as the first argument.
In contrast to call/cc, abort-to-prompt unwinds the call stack up to the call-with-prompt context and does not continue with the result of the handler. It only continues if the handler explicitly calls the continuation.
Here is a small example that demonstrates how this mechanism could be used to implement a cooperative scheduler.
(define async-tag (make-prompt-tag))
;; Yield control back to the scheduler, pushing our continuation to the back of the queue
(define (yield)
(abort-to-prompt async-tag))
(define (run-scheduler thunks)
;; Queue of ready tasks (thunks or continuations)
(define queue thunks)
(define (enqueue task)
(set! queue (append queue (list task))))
(let loop ()
(when (not (null? queue))
(let ((current-task (car queue)))
(set! queue (cdr queue))
;; Run the current task bounded by our prompt tag
(call-with-prompt async-tag
current-task
(lambda (cont)
;; Handler runs on (yield): enqueue the captured continuation
(enqueue (lambda () (cont))))))
(loop))))
;; Test Tasks
(define (thunk-1)
(display "thunk-start-1\n")
(yield)
(display "thunk-cont-1\n"))
(define (thunk-2)
(display "thunk-start-2\n")
(yield)
(display "thunk-cont-2\n"))
(run-scheduler (list thunk-1 thunk-2))The output is
thunk-start-1
thunk-start-2
thunk-cont-1
thunk-cont-2I hope you can already imagine how this mechanism could be applied to an event loop for asynchronous IO. The handler runs the event loop and call the body to start the IO operations. The body calls abort-to-prompt whenever it has to wait for IO. The handler receive the continuations and schedules them with the event loop so that they are called when the IO operations complete.
Prompts can be used for other non-local control operations as well. Guile's exception handling uses prompts under the cover.
Conclusion
If you made it here, you can probably understand the initial examples with ease.
My hope is that the examples give you a flavor of what's possible with Guile Scheme and provide a starting point for your own projects. In case of doubt, this post should help you to ask your favorite LLM the right questions. Any of the frontier models is more than capable of writing correct and idiomatic Guile Scheme code for whatever software problem you wan to solve.