Preface
Goal: A practical case to collect unique record fields using Guile.
The beginner’s issues in learning Guile language are
- Lack of example in the internet, not even stackoverflow.
- 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
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
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
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
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
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
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)
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") )
))
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))
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
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 ].