A Guile Mind Book
This work is free work under cc-by-nc-sa, you can contribute.
Introduction
This is a work in progress book about GNU Guile Scheme. It's goal is to broaden the horizon of the reader and hopefully help them become a Guile hacker. It is not a perfect academical course about Scheme or Guile or even programming. Prior knowledge about programming is required. Knowledge of Python, Javascript or Ruby is good enough. It aims to help the average Jane and Joe developer to get started with GNU Guile and fight boredom with the help of lot of parentheses.
An important note is that this book doesn't claim that you will feel englightened after reading it.
Why Guile?
Nowdays they are dozens of (exotic?) progamming languages that you could learn so you might be wondering: Why GNU Guile is a good choice? I don't know for you, but here are the reasons why I learned it:
- It's a GNU project. As such, programming in Guile is contributing to it. As a free software advocate, I was looking for a way to contribute back that had more impact than simply using a GNU license.
- It's a Scheme which follows the Scheme standard. This means that what you (will?) learn in Guile can be somewhat re-used to learn another Scheme. For instance, it allowed me to jump into BiwaScheme, a Scheme interpreter written in Javascript, which in turns allowed me to understand better modern web frameworks like React, Elm or Vue.
- Guile is primarly a functional language. In this way, it provides various facilities to work with immutable data structures, but it doesn't forbid mutation either. It also has a framework for doing Object Oriented Programming rather differently than what you are used to but still familiar enough. So, you will learn new things without leaving too much your confort zone.
Also Guile has no Global Interpreter Lock, which means that you can fully take advantage of multiple cores. It has powerful lightweight thread facility albeit experimental. Guile has a good scientifical background. It has powerful metaprogramming facilities through macros and a powerful Object Oriented Programming framework.
Tutorial
Installing GNU Guix
It is recommended to install guix package manager because that is the
primary way to acquire Guile modules. Take a look at
Guix
installation instructions
and put every Guix environment variable under ~/.guix
.
Then source that file when you want to work on Guile stuff.
Guile 2.2 is also available in many distros, so for the tutorial you will not need more dependencies than the ones that come with it.
Getting started
Bootstraping
To start following the tutorial, install guile and launch the REPL
with guile
command. The following line of text will be
displayed:
scheme@(guile-user)>
You can type for instance 1985
and you will now see the
following:
scheme@(guile-user)> 1985
$1 = 1985
Introduction
In this tutorial you will study how to:
- call a procedure
- define a variable
- create a list
- apply a procedure to a list
- create pairs
- create a scheme dictionary aka. association list aka. alist
- define a procedure
- create a new list from an initial list and a procedure using map
Let's go!
Calling a procedure
A procedure is equivalent to what other languages call a function. In Guile, for example, the addition is a procedure. To add 27 to 15 you can do:
scheme@(guile-user)> (+ 27 15)
$2 = 42
There is also a minus procedure named -
and times named
*
, so Guile is a perfect calculator and supports
arbitrary big numbers. If this doesn't speak to you, suffice to say
it's very useful for doing science.
Imagine that you have 101 donuts for the year. You'd like to know how many donuts you can eat per hour without running out of them. You solve this big problem using the following code:
scheme@(guile-user)> (/ 101 (* 24 365))
$3 = 101/8760
As you can see it returns an exact number. To convert the results to
something that is more readable, we can ask Guile to use the procedure
exact->inexact
:
scheme@(guile-user)> (exact->inexact $3)
$4 = 0.011529680365296804
In the above $3
references the result of
(/ (* 24 365) 101)
. If the result count is not the same
as the one used above, replace $3
with the value you see.
REPL
The Read-Eval-Print-Loop aka. REPL is a very useful tool.
The default configuration doesn't use readline. You can activate it
using a $HOME/.guile
configuration file. Create it and
copy/paste the following:
(use-modules (texinfo reflection)) ;; help
(use-modules (ice-9 readline))
(activate-readline)
and restart the REPL.
You can try using up and down arrows to navigate the history. Use TAB to complete the current input. For instance, if you type exact and hit TAB, it will display a list of procedures that start with it.
You can now also use (help something)
to get the
documentation of SOMETHING
.
Toplevel variable definition
The first way to define variables is using the
define
form:
scheme@(guile-user)> (define guilers 1337)
define
returns nothing, that's why there no line with a
dollar sign.
At the next Guile hackfest 1337 guilers will gather to hack the final cosmits for the Earth Software System. Every hacker is given an apple, three donuts and two chais. How many apples, donuts and chais are needed?
scheme@(guile-user)> (define apple-per-guiler 1)
scheme@(guile-user)> (define apples (* apple-per-guiler guilers))
scheme@(guile-user)> apples
$15 = 1337
scheme@(guile-user)> (define donut-per-guiler 3)
scheme@(guile-user)> (define donuts (* donut-per-guiler guilers))
scheme@(guile-user)> donuts
$16 = 4011
scheme@(guile-user)> (define chai-per-guiler 2)
scheme@(guile-user)> (define chais (* chai-per-guiler guilers))
scheme@(guile-user)> chais
$17 = 2674
How much food in total will be given? Try to guess how to compute the total...
scheme@(guile-user)> (+ chais donuts apples)
$18 = 8022
Got it?
List List List
Scheme is made of list. Maybe you did not recognize it but the parens and what's inside the parens separated by space form a list.
To build your own list you can use the list
procedure:
scheme@(guile-user)> (list apples donuts chais)
$19 = (1337 4011 2674)
To retrieve the head of the list you can use
car
procedure:
scheme@(guile-user)> (car (list apples donuts chais))
$20 = 1337
Whereas cdr
procedure will retrive the tail:
scheme@(guile-user)> (cdr (list apples donuts chais))
$21 = (4011 2674)
Ok?
apply
Say you already have a list that you want to pass as arguments to a
procedure. How do you do? Well, for that you use the
(apply proc lst)
procedure, which takes the list
LST
as arguments to apply to the procedure
PROC
.
For instance you can compute the sum of a list of integers using:
scheme@(guile-user)> (define everything (list apples donuts chais))
scheme@(guile-user)> (apply + everything)
$22 = 8022
Strings
Strings in Guile are similar to strings in other languages. The single particular thing is that you can only define strings with double quotes:
scheme@(guile-user)> (define box (list "1 apples" "3 donuts" "2 chais"))
This is a pretty simple definition for the box.
Associations
Another important data structure of Guile is the association list.
It's actually only a list of pairs. A pair is constructed with
cons
procedure, for instance:
scheme@(guile-user)> (cons "apple" 1)
$20 = ("apple" . 1)
This means that "apple" is associated with 1.
Together, cons
, car
and cdr
are
list primitives. Higher level procedures exist to deal with more
complex situations.
An association is a list of cons.
To define a better representation for the box, we can use the following code:
scheme@(guile-user)> (define box (list (cons "apple" 1) (cons "donuts" 3) (cons "chais" 2)))
scheme@(guile-user)> box
$21 = (("apple" . 1) ("donuts" . 3) ("chais" . 2))
scheme@(guile-user)>
Now we can retrieve the number of chai in a box using
assoc-ref
procedure:
scheme@(guile-user)> (assoc-ref box "chais")
$22 = 2
Usually the first argument of a procedure is the primary object, which is the object against the action is taken.
Toplevel procedure definition
define
is also used to define a procedure but with a
small syntax change.
Remember that to define a variable the syntax is the following:
(define answer 42)
Let's define ruse
procedure that returns itself:
scheme@(guile-user)> (define (ruse) ruse)
scheme@(guile-user)> ruse
$24 = #<procedure ruse ()>
scheme@(guile-user)> (ruse)
$25 = #<procedure ruse ()>
scheme@(guile-user)>
This procedure is not useful, except to explicit the syntax of
define
to define a procedure. The astute reader has noted
that the procedure takes no argument. Such procedure is called a
thunk. Calling a procedure you defined is done the same way
as regular procedures. In this case, the procedure takes no arguments
so it looks different but the principle is the same.
Let's define a procedure that takes two arguments and returns their mean:
scheme@(guile-user)> (define (mean a b) (/ (+ a b) 2))
scheme@(guile-user)> (mean 12 12)
$29 = 12
Mind the fact that space and newlines have no effect on the interpretation of Scheme code. So the above one liner can be written as follows:
scheme@(guile-user)> (define (mean a b)
(/ (+ a b) 2))
Let's try something more complex.
map
procedure
One of the most important procedures of Guile is
(map proc lst)
, which iterates over a list and applies a
procedure over each item. Given a list (list a b c)
it
will return a new list (list (proc a) (proc b) (proc c))
.
For instance, we can increment a list of integers:
scheme@(guile-user)> (map 1+ (iota 5))
$30 = (1 2 3 4 5)
Mind the fact that $30
is a new list. Try this:
scheme@(guile-user)> (define numbers (iota 5))
scheme@(guile-user)> (define others (map 1+ numbers))
scheme@(guile-user)> (equal? numbers others)
$31 = #f
scheme@(guile-user)> numbers
$32 = (0 1 2 3 4)
scheme@(guile-user)> others
$33 = (1 2 3 4 5)
You can see that map
creates a new list based on the
input list.
Making list out of a list
To finish the introduction to the basics we will define a procedure that will simulate a guiler picking a chai from her box. Remember the box is defined as an association:
scheme@(guile-user)> (define box (list (cons "apple" 1) (cons "donuts" 3) (cons "chais" 2)))
scheme@(guile-user)> box
$21 = (("apple" . 1) ("donuts" . 3) ("chais" . 2))
scheme@(guile-user)>
The association is a list, so it can go through
map
procedure. We will mock the procedure that we want to
implement:
scheme@(guile-user)> (define (pick-chai box)
(map pair-pick-chai box))
pair-pick-chai
takes an item of the box ie. a pair,
that's why it's prefixed with pair-. It must decrement the count of
chais if it's a "chai" pair or return the pair as-is if it's not. Will
need a conditional branch if.
if
syntax is the following:
(if <test>
<then>
<else>)
Live it looks like this:
scheme@(guile-user)> (if #true "ok" "ko")
$34 = "ok"
So, if we also use car
and cdr
, you may
guess that pair-pick-chai
can be defined as:
scheme@(guile-user)> (define (pair-pick-chai pair)
(if (equal? (car pair) "chais")
(cons "chais" (1- (cdr pair)))
pair))
That's all! Well almost... This is a bit naive because there might be no chai left. Maybe you can find how to solve this issue?
Wrapping up
In the first part of this tutorial you studied the basics of Guile:
-
how to call a procedure
(string->list "abcdefghijklmnopqrstuvxyz")
-
how to define a Scheme variable:
(define answer 42)
-
how to create a Scheme list :
(list "abc" ruse 22)
- how to create Scheme pairs:
(cons "guile" 1)
-
how to create Scheme associations:
(list (cons "functional" 1) (cons "OOP" 2))
-
how to define a Scheme procedure:
(define (decrement count) (- count 1))
-
how to use Scheme
(map proc lst)
to create a new list out of a list and procedure
Going forward
Introduction
Backtracking
In the first tutorial we studied the basics of Guile. We studied:
-
how to call a procedure
(string->list "abcdefghijklmnopqrstuvxyz")
- how to define a variable
(define answer 42)
- how to create a list
(list "abc" ruse 22)
- how to create pairs
(cons "guile" 1)
-
how to create associations
(list (cons "functional" 1) (cons "OOP" 2))
-
how to define a procedure
(define (decrement count) (- count 1))
-
how to use
(map proc lst)
to create new list out of a list and procedure
Continuation
In this tutorial you will study:
- how to solve the last exercise
- how to define variables inside procedures
- a shorthand to make recursive procedures
- how to define mutable datastructures
Picking up breakfast
In the last tutorial we offered a breakfast box for every guiler. Remember it looked like the following:
scheme@(guile-user)> (define box (list (cons "apple" 1) (cons "donuts" 3) (cons "chais" 2)))
We created a procedure that picked a chai from the box, but it was picking something even if nothing was in the box:
scheme@(guile-user)> (define (pick-chai box)
(map pair-pick-chai box))
;;; <stdin>:202:23: warning: possibly unbound variable `pair-pick-chai'
scheme@(guile-user)> (define (pair-pick-chai pair)
(if (equal? (car pair) "chais")
(cons "chai" (1- (cdr pair)))
pair))
Now when we apply the first procedure, it really pick a chai, but
doesn't stop picking when there is no more chai, which leads to the
strange situations where there is -1
chai in the box:
scheme@(guile-user)> (define box-v2 (pick-chai box))
scheme@(guile-user)> box-v2
$39 = (("apple" . 1) ("donuts" . 3) ("chais" . 1))
scheme@(guile-user)> (define box-v3 (pick-chai box-v2))
scheme@(guile-user)> box-v3
$40 = (("apple" . 1) ("donuts" . 3) ("chais" . 0))
scheme@(guile-user)> (define box-v4 (pick-chai box-v3))
scheme@(guile-user)> box-v4
$41 = (("apple" . 1) ("donuts" . 3) ("chais" . -1))
Remember that (map proc lst)
returns a new list, hence
pick-chai
returns a new version of the box where one chai
was picked. That's why we store the new version of the box and always
pass its latest version to pick-chai
to pick several
chai.
We should only pick a chai if there is actually a chai to pick. So we
will rewrite the pair-pick-chai
procedure and introduce
the procedure pair-maybe-pick-chai
that will return an
empty chai pair if there's no more chai or a chai pair with one less
chai if there is still some chai:
scheme@(guile-user)> (define (pair-pick-chai pair)
(if (equal? (car pair) "chais")
(pair-maybe-pick-chai pair)
pair))
;;; <stdin>:414:27: warning: possibly unbound variable `pair-maybe-pick-chai'
scheme@(guile-user)> (define (pair-maybe-pick-chai pair)
(if (equal? (cdr pair) 0)
(cons "chais" 0) ;; there is no more chai
(cons "chais" (- (cdr pair) 1))))
box-v3
is the box version which has no more chai. Let's
try (pick-chai box-v3)
. As you will see, the REPL will
use the new pair-pick-chai
procedure defined above that
makes use of pair-maybe-pick-chai
:
scheme@(guile-user)> box-v3
$46 = (("apple" . 1) ("donuts" . 3) ("chai" . 0))
scheme@(guile-user)> (define box-v4.2 (pick-chai box-v3))
$47 = (("apple" . 1) ("donuts" . 3) ("chai" . 0))
scheme@(guile-user)> (equal? box-v3 $47)
$48 = #t
As you see box-v3
and $47
are equal, which
means that no chai was picked from the box.
Variable bindings
Variable bindings is the construction Guile uses to define variables
inside other constructions. This can be done thanks to
let*
, its syntax is the following:
(let* ((variable-one value-one)
(variable-two value-two)
...)
(expression-one)
(expression-two)
(return-value))
The first list that follows let*
looks like a literal
assoc without the dot between the variable key and the value. For
instance, you can try the following in the REPL:
scheme@(guile-user)> (let* ((chai 2)
(donut 3)
(apple 1))
(+ chai donut apple))
$49 = 6
By the way, it's not very useful in the REPL.
We can write a box-mean
procedure that computes the mean
count of food in the box:
scheme@(guile-user)> (define (box-mean box)
(let* ((chais (assoc-ref box "chais"))
(donuts (assoc-ref box "donut"))
(apple (assoc-ref box "apple"))
(total (+ chai donuts apple)))
(/ total 3)))
scheme@(guile-user)> (box-mean box)
$53 = 2
See what was done? The values are returned by a procedure and
total
re-uses the previously defined bindings.The star *
in let*
means that it's the improved version of
let
. let
without star doesn't allow to
reference bindings defined in the same binding block.
Named let
Obviously, it's possible to do recursive calls with procedures that
are defined using define
. Using recursive behavior is
widepread. Guile provides a helper syntax in the form of the
named-let. It replaces the following syntax:
(define (recursive a)
(if (equal? a 0)
"that's all!"
(recursive (- a 1))))
(recursive 5)
With the following one:
(let recursive ((a 5))
(if (equal? a 0)
"that's all!"
(recursive (- a 1))))
It is useful in situations where you would have defined a separate procedure to implement the recursive behavior.
Good! We need a way to unbox the breakfast. Instead of an association, we'd like to have the food in a list appearing as many times as they appear in the box.
Let's try to use map
. map
takes pairs, so
unbox will look like the following:
(define (unbox box)
(map pair-unbox box))
I hope you see what will happen. Every pair must be turned into a list
with correct count of food. Let's define pair-unbox
:
(define (pair-unbox pair)
(let loop ((food (car pair))
(count (cdr pair))
(plate (list)))
(if (equal? count 0)
plate
(loop food (- count 1) (cons food plate)))))
Little you knew that cons
could also build lists...
But what does this code work?! REPL to the rescue! Let's try it:
scheme@(guile-user)> (pair-unbox (cons "apple" 2))
$65 = ("apple" "apple")
So, now that we have everything in place we can try this first version
of unbox
:
scheme@(guile-user)> (define (unbox box)
(map pair-unbox box))
scheme@(guile-user)> (unbox box)
$68 = (("apple") ("donut" "donut" "donut") ("chai" "chai"))
As you can see, this doesn't look like a plate: they are nested lists.
The simplest solution to solve this issue is to use
apppend-map
procedure instead of map
.
Defining modules
Fire you favorite emacs editor and open a new file named
ess.scm
.
Modules are defined using (define-module (o/))
where
o/
must be replaced with the module path of the defined
module. In this case the ess
module is a root module so
you only have to add (define-module (ess))
to the top of
the file.
If for instance, later, maybe, one day, we create a
ess/spaceship.scm
module, it will be defined with
(define-module (ess spaceship))
.
Importing other modules
To import a module you have to use the use-modules
form:
(use-modules (intrisic thruth))
It will import everything that is public in that module.
They are numerous modules in Guile. A lot of them come from SRFI and rnrs specifications, while some others are specific to Guile.
Let's try the standard list module named srfi-1
:
scheme@(guile-user)> (use-modules (srfi srfi-1))
scheme@(guile-user)> (last box)
$54 = ("chais" . 2)
scheme@(guile-user)> (first box)
$55 = ("apple" . 1)
scheme@(guile-user)> (first (last box))
$56 = "chais"
How to define mutable records
Lists (and assocs) are useful but are not the best mean for every end. For the situations where a list is not enough there are records. Records come in different flavors in Guile. Here we will use mutable records to explicit the fact that Guile can also work with mutable datastructures.
To use records you must add
(use-modules (srfi srfi-9))
to the top of
ess.scm
.
The Earth Software System needs a TODO application that we will implement in the following paragraphs.
A TODO application is a list made of TODO items. We will describe a TODO item as a title and a status.
We will define an <item>
record using
define-record-type
. This form is a macro! Suffice to say,
the abstract syntax tree is reworked by a procedure sometimes before
execution. The final result is that the code you write is not executed
like regular code but in some other way. In this case, the elements
found in define-record-type
"kind of list" are not
expressions, so they are not executed like expressions but are
specified as a record.
If this doesn't make sense to you, just consider that
define-record-type
is a keyword followed by a specific
syntax, even if in pratice, implementation wise, it's a kind of
procedure:
(define-record-type <item>
(make-item title status)
item?
(title item-title-ref item-title-set!)
(status item-status-ref item-status-set!))
The above define-record-type
named
<item>
has:
make-item
as constructor- two fields
title
andstatus
item?
as predicate-
title
has(item-title-ref item)
as getter and(item-title-set! item title)
as setter -
Similarly,
status
has as getter and setter respectivelyitem-status-ref
anditem-status-set!
Let's define a few tasks:
(define todo (list (make-item "build earth software system" "ongoing")
(make-item "build gnunet backed database" "not started")
(make-item "find the answer" "postponed")
(make-item "make a mini todo list application" "not started")))
We want a procedure to print the content of the TODO list. For that, we can use the following:
(define (item-print item)
(format #true "~a: ~a" (item-title item) (item-status item)))
(define (todo-print todo)
(map item-print todo))
We will create a procedure that allows to set an item as
"done"
:
(define (item-done! item)
(item-status-set! item "done"))
The !
suffix means that the procedure mutates the
<item>
.
Let's add an expression putting all this together:
(item-done (list-ref todo 2))
(todo-print todo)
Ok?
Wrapping Up
In this tutorial we:
- Solved the exercice of the previous tutorial with a slighly more complex code.
-
Discovered that defining variables inside procedures is done
differently via
let*
form. - Studied a shorthand to define recursive procedures called the named-let.
-
Defined records, in this case mutable records aka. records that are
modified in place via
define-record-type
macro.
Homework
Try to make a cryptocurrencies converter, for example coindataflow.