hyperdev.fr

Going forward with Guile Scheme

Introduction

Backtracking

In the first tutorial we studied the basics of Guile. We studied:

Continuation

In this tutorial you will study:

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 "chai" 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) "chai")
      (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) ("chai" . 1))
scheme@(guile-user)> (define box-v3 (pick-chai box-v2))
scheme@(guile-user)> box-v3
$40 = (("apple" . 1) ("donuts" . 3) ("chai" . 0))
scheme@(guile-user)> (define box-v4 (pick-chai box-v3))
scheme@(guile-user)> box-v4
$41 = (("apple" . 1) ("donuts" . 3) ("chai" . -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) "chai")
                           (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 "chai" 0)  ;; there is no more chai
                           (cons "chai" (- (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 use 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.

For instance 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* ((chai (assoc-ref box "chai"))
                                  (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 procedures and total re-use, the previously defined bindings. The latter is a specific feature of let*. 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 call 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 replace the following syntax:

(define (recursive a)
  (if (equal? a 0)
    "that's all!"
    (recursive (- a 1))))
(recursive 5)

With the following syntax:

(let recursive ((a 5))
  (if (equal? a 0)
      "that's all!"
      (recursive (- a 1))))

It 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 list appearing as many times they appear in the box.

Let's try to use map. So 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 did you know that cons could also build lists...

But what does this code?! REPL to the rescue! Let's try this code:

scheme@(guile-user)> (pair-unbox (cons "apple" 2))
$65 = ("apple" "apple")

So now we have everything in place we can try this first 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/hyperloop.scm module. It will be defined with (define-module (ess hyperloop)).

Importing other modules

To import a module, you have to use the use-modules form:


(use-modules (intrisic thruth))

It will import everything from that module if it exist.

They are numerous modules in Guile a lot of them come from srfi and rnrs specifications. Some are specific to Guile.

Let's try the standard list module namely srfi 1:

scheme@(guile-user)> (use-modules (srfi srfi-1))
scheme@(guile-user)> (last box)
$54 = ("chai" . 2)
scheme@(guile-user)> (first box)
$55 = ("apple" . 1)
scheme@(guile-user)> (first (last box))
$56 = "chai"

Mutable Guile (or how to define mutable records)

Lists (and assoc) are useful but are not the best mean for every end. For the situations where a list is not enough they are records. Records comes 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 appliation, we will implement one in the following paragraph.

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 form. That form is a macro! Suffice to say, that the abstract syntax tree is reworked by a procedure sometime before execution and that the end 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 and as such are not executed like expressions but are specification of the record.

If this doesn't make sens 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)
  atom?
  (title item-title-ref item-title-set!)
  (status item-status-ref item-status-set!))

The above define a record type named <item> which has:

Let's define a few task:

(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 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 mutate 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 studied: