Article Series

This article series discuss more than 30 different programming languages. Please read overview before you read any of the details.

Playing with Records Related Articles.

Preface

Goal: A practical case to collect unique record fields using Guile.

The beginner’s issues in learning Guile language are

  1. Lack of example in the internet, not even stackoverflow.
  2. Lack of community, I have nowhere to ask.

Personal Motivation

After learning TCL/TK, I decide to step in into its very contender, the Guile programming language. Learning obscure language, especially with exotic syntax such as Guile, give me different perspective.

I have to switch from imperative to functional, which is a good thing. But there is more than that, I have to walk the walk, before understand the basic concept. This require more basic example, than other language.

So here it is my learning process, to make it easier for you to learn the Guile language.

Indentation

Forgive my indentation. I’m coming from curly braces world.

Reference Reading

The last time I read guile documentation thoroughly, was two decades ago.

Source Examples

You can obtain source examples here:


Common Use Case

Task: Get the unique tag string

Please read overview for more detail.

Prepopulated Data

Songs and Poetry

(define-module (my-songs))

(define-public songs (list
 '( ("title" . "Cantaloupe Island")
    ("tags"  . ("60s" "jazz")) )
 '( ("title" . "Let It Be")
    ("tags"  . ("60s" "rock")) )
 '( ("title" . "Knockin' on Heaven's Door")
    ("tags"  . ("70s" "rock")) )
 '( ("title" . "Emotion")
    ("tags"  . ("70s" "pop")) )
 '( ("title" . "The River") )
))

Guile Solution

The Answer

I consider functional approach than imperative one, using map and grep, and then recursive subroutine. This functional approach is very suitable with lisp syntax. The result is a little bit long, as below:

(add-to-load-path
  (dirname (current-filename)))
(use-modules (srfi srfi-1))
(use-modules (my-songs))

(define (flatten my-songs)
  (let ((tagss
    (map (λ (song) (cdr (assoc "tags" song)))
      (filter (λ (song) (assoc "tags" song))
      my-songs))
    ))
    (concatenate tagss)))

(define (unique tags)
  (if (<= (length tags) 1)
    tags
    (let
      ( (head (car tags)) (tail (cdr tags)) )
      (append (list head)
        (unique (delq head tail)))
)))

(display (string-join
  (unique (flatten songs)) ":"))
(newline)
  • Enough with introduction, at this point we should go straight to coding.

Environment

No need any special setup. Just run and voila..!


1: Simple List

We can use list or quote or just braces. I still don’t know the different between this three.

Dump Output

Consider begin with simple list.

(define tags (list
  "rock" "jazz" "rock" "pop" "pop"))

(define tags-str
  (string-join tags ":"))
(display tags-str)
(newline)

(format #t "Head: ~s\n" (car tags))
(format #t "Tail: ~a\n" (cdr tags))

(map display tags)
(newline)

You can print in guile using either display or format. With the result similar as below list:

❯ guile 01-tags-a.scm
rock:jazz:rock:pop:pop
Head: "rock"
Tail: (jazz rock pop pop)
rockjazzrockpoppop

Guile: A very simple list

We are going to utilize string-join through out this article.

Iterate List

Alternatively, you can dump using map or for-each. map can be used to output value, while for-each focus on side effect. However, you can also display inside map, which is useful for debugging.

(define tags '(
  "rock" "jazz" "rock" "pop" "pop"))

(map (lambda tag 
  (display (car tag))
  (display (cdr tag))
) tags)
(newline)

(for-each (lambda (tag)
  (display tag)
) tags)
(newline)

The result is similar as below list:

❯ guile 01-tags-b.scm
rock()jazz()rock()pop()pop()
rockjazzrockpoppop

Guile: Map and For Each


2: Building Data Structure

Extract structure and display as string.

Before we dive deep into any algorithm, we need examine data structure first.

Simple List

Consider back to our simple list, as our foundation.

(define tags (list
  "rock" "jazz" "rock" "pop" "pop"))

(define tags-str
  (string-join tags ":"))
(display tags-str)
(newline)

The result is similar as below list:

❯ guile 02-str-a.scm
rock:jazz:rock:pop:pop

Guile Structure: Simple List

List of Quote

Consider enhance our example as a kind of array of array structure. There many alternative in Guile, the choice is up to you. This time, we can pick list of quote as an example.

(use-modules (srfi srfi-1))

(define tags (list
  '("60s" "jazz")
  '("60s" "rock")
  '("70s" "rock")
  '("70s" "pop")
))

(define tagss (concatenate tags))
(display tagss)
(newline)

(define tags-str
  (string-join tagss ":"))
(display tags-str)
(newline)

The result is similar as below list:

❯ guile 02-str-b.scm
(60s jazz 60s rock 70s rock 70s pop)
60s:jazz:60s:rock:70s:rock:70s:pop

Guile Structure: List of Quote

We can simply flatten this structure using (concatenate tags), which is available in (srfi srfi-1) module.

Mimic Hash (or Dictionary)

Using Constructor

However, we can construct hash like structure, all with tags key. We can extract using map, and get new structure.

(use-modules (srfi srfi-1))

(define tags (list
  (cons "tags" (list "60s" "jazz"))
  (cons "tags" (list "60s" "rock"))
  (cons "tags" (list "70s" "rock"))
  (cons "tags" (list "70s" "pop"))
))

(define tagss
  (map
    (λ (tagss) (cdr tagss))
    tags)
)

(display tagss)
(newline)

(display (string-join
  (concatenate tagss) ":"))
(newline)

The result is similar as below list:

❯ guile 02-str-c.scm
((60s jazz) (60s rock) (70s rock) (70s pop))
60s:jazz:60s:rock:70s:rock:70s:pop

Guile Structure: Mimic Hash or Dictionary using Constructor

You should be careful with the structure. Pay attention, why I put the (cdr tagss). I took me hours to debug Guile, until I understand the concept.

Real Hash (or Dictionary)

The Real One

Consider make the structure more complex.

(use-modules (srfi srfi-1))

(define tags (list
  '( ("tags" . ("60s" "jazz")) )
  '( ("tags" . ("60s" "rock")) )
  '( ("tags" . ("70s" "rock")) )
  '( ("tags" . ("70s" "pop"))  )
))

(define tagss
  (map
    (λ (tagss) (assoc-ref tagss "tags"))
    tags)
)

(display tagss)
(newline)

(display (string-join
  (concatenate tagss) ":"))
(newline)

And examine how to access the array inside the hash. Now we can apply (assoc-ref tagss "tags").

(define tagss
  (map
    (λ (tagss) (assoc-ref tagss "tags"))
    tags)
)

The result is similar as below list:

❯ guile 02-str-d.scm
((60s jazz) (60s rock) (70s rock) (70s pop))
60s:jazz:60s:rock:70s:rock:70s:pop

Guile Structure: Real Hash or Dictionary Structure

Notes on Pairs

Below are equivalent

(a b)

and

(a . (b ))

3: Separating Module

Since we need to reuse the songs record multiple times, it is a good idea to separate the record structure from logic.

Data Structure Using Dictionary

Simple Record

Consider try another structure, a hash to store just one record.

(define song '(
  ("title" . "Cantaloupe Island")
  ("tags" . (list "60s" "jazz"))
))

(display song)
(newline)

(display (car song))
(newline)

(display (cdr (assoc "tags" song)))
(newline)

Now, examine the result:

❯ guile 03-record.scm
((title . Cantaloupe Island) (tags list 60s jazz))
(title . Cantaloupe Island)
(list 60s jazz)

Guile Structure: One Record

The Songs Structure

Multiple Records

We can continue our journey to records using list of hash. No need any complex structure.

(define-module (my-songs))

(define-public songs (list
 '( ("title" . "Cantaloupe Island")
    ("tags" . (list "60s" "jazz")) )
 '( ("title" . "Let It Be")
    ("tags" . (list "60s" "rock")) )
 '( ("title" . "Knockin' on Heaven's Door")
    ("tags" . (list "70s" "rock")) )
 '( ("title" . "Emotion")
    ("tags" . (list "70s" "pop")) )
 '( ("title" . "The River") )
))

(use-modules (my-songs))

(map (lambda (song)
  (format #t "~a\n" (assoc "tags" song))
) songs)

Notice the define-public keyword here, as we prepare to move the structure into its own module.

(define-public songs (list ... ))

The result of the script will be similar as below record:

❯ guile 03-songs.scm
(tags list 60s jazz)
(tags list 60s rock)
(tags list 70s rock)
(tags list 70s pop)
#f

The last one do not have tags key, thus the result is false #f.

The Songs Module

Notice how easy the transition. All we need to add just the define-module keyword.

(define-module (my-songs))

(define-public songs (list
 '( ("title" . "Cantaloupe Island")
    ("tags"  . ("60s" "jazz")) )
 '( ("title" . "Let It Be")
    ("tags"  . ("60s" "rock")) )
 '( ("title" . "Knockin' on Heaven's Door")
    ("tags"  . ("70s" "rock")) )
 '( ("title" . "Emotion")
    ("tags"  . ("70s" "pop")) )
 '( ("title" . "The River") )
))

Guile: The Songs Module Containing Array of Records

Module in Relative Path

In order to use module in relative path, we need to add a few more header declaration.

(add-to-load-path
  (dirname (current-filename)))
(use-modules (my-songs))

Guile: Using Songs Module

Using Songs Module

Now we can have a shorter code as shown below.

(add-to-load-path
  (dirname (current-filename)))
(use-modules (my-songs))

(map (lambda (song)
  (format #t "~a\n" (assoc "tags" song))
) songs)

With the result exactly the same as previous code.

❯ guile 04-module.scm
(tags 60s jazz)
(tags 60s rock)
(tags 70s rock)
(tags 70s pop)
#f

Guile: Using Songs Module


What is Next 🤔?

We need to continue extracting the data structure, until we finish our unique challenge. We have to approach: imperative, or functional.

Consider continue reading [ Guile - Playing with Records - Part Two ].