In Defining variables, you met
define, which defines global variables. These variables are called “global” because they are available everywhere. The syntax shown here, on the other hand, binds “local” variables, which are only temporarily valid in one section of the code. Here is a program that does not work:
(define composer "Mozart") (if (equal? composer "Mozart") (begin (define birth 1756) (format #t "Mozart was born in ~a, his 300th birthday will be in ~a." birth (+ birth 300))))
(NB: At least, this program will not work with Guile 2.2. The rules over the placement of
defines are complex, haved changed in the history of Guile, and are still not stabilized.)
The reason this does not work is that
birth is defined as a top-level variable, but its binding occurs in an expression, which is only evaluated under certain conditions.
Ths is why
define is only useful at the toplevel, to define functions, or constants such as
(define PI 3.1415926535)
For temporary variables which used to obtain a result but no longer result after the result has been computed, a different syntax is used. It uses the
let keyword. Here is a syntax diagram:
(let ((variable1 value1) (variable2 value2) (variable3 value3) ...) expression1 expression2 ...)
let is executed, the values are first evaluated and bound to all the variables. Then, the expressions are evaluated in order, and the
let expression evaluates to the value of the last expression inside it, just like with
begin. After the last parenthesis of
let has been closed, the variables no longer exist. In technical terms, they are bound in the scope of the
The following examples use the
random function. The call
(random n) return a random integer between \(0\) and \(n - 1\). Of course, you might get different results than the ones shown here, since they are random.
(define (loto) (let ((x (random 2)) (y (random 2))) (display (if (and (= x 1) (= y 1)) "Chance!" "You lost!")))) (loto) ⊨ You lost!
Here, two coins are flipped (heads = 0, tails = 1). If both show tails, the user wins.
let form has a recognizable visual shape:
(let (xxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxx) xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx)
Scheme programmers, without ever counting parentheses, recognize the block of variables and the block of expressions.
Let’s invent a variant of the game, where a dice is first rolled, which gives the number of balls in a box, then one ball is drawn from the box at random, given that there is exactly one winning ball. For example, if the dice gives 4, there will be 4 balls in the box and thus a probability of 1/4 to win. It is tempting to write this:
(define (loto2) (let ((number-of-balls (random 10)) (ball (random number-of-balls))) (display (if (equal? ball 0) "Chance!" "You lost."))))
Yet, this gives the error “Unbound variable: number-of-balls”. This is because
let is actually quirky. First, the values for all variables are evaluated, and only then, they are bound to the variables. The interpreter tries to evaluate
(random 10), then
(random number-of-balls), and afterwards bind the variables
ball to these two values. Of course, we would like
number-of-balls to be bound before
(random number-of-balls) is computed. In this case,
let needs to be replaced with a variant of it,
let*, of which the syntax is exactly the same. The corrected example is:
(define (loto2) (let* ((number-of-balls (random 10)) (ball (random number-of-balls))) (display (if (equal? ball 0) "Chance!" "You lost.")))) (loto2) ⊨ You lost.
let* first evaluates the first expression and stores it in the first variable, and only then evaluates the expression (which can thus reuse the first variable) and stores it in the second variable, etc.
In practice, you almost most often want to use
let* expressions contain many parentheses. It is easy for the unexperienced to get them wrong. This part goes through all common parenthesizing errors to explain them. I will use this example:
(let ((a 5)) (+ a 15))
Forgetting a parenthesis.
(let ((a 5) ; missing ) (+ a 15))
With this expression in a LilyPond file (with a preceding
#to introduce Scheme code), you will get the error “end of file”, which means that the Scheme expresson never ended.
Adding an extraneous parenthesis.
(let ((a 5)) (+ a 15))) ; extra )
In LilyPond (don’t forget the
#), the error may seem more surprising: “syntax error, unexpected EVENT_IDENTIFIER”. What happens is that when the expression ends, LilyPond syntax is used again. At this point, the extraneous parenthesis is parsed. In LilyPond, parentheses are the syntax for slurs, hence the message indicating that a slur is not valid on the toplevel.
Moving a parenthesis.
(let ((a 5) ; missing ) (+ a 15))) ; extra )
Here, there is no extraneous or missing parenthesis for the expression as a whole; its parentheses are balanced. You text editor will thus not find the mistake. Yet, a parenthesis is misplaced. The error message is somewhat short: “bad let”. To understand it, let us come back to how a
(let (xxxxxxxxx xxxxxxxxx) xxxxxxx xxxxxxx)
(...)contains all bindings. Here, this
(...)actually contains everything that’s inside the
letexpression. The code could be reformatted like this:
(let ((a 5) (+ a 15)) )
The interpreter tries to see
(+ a 15)as
(variable-name value), which fails because there are three elements between the parentheses rather than two. The
letis also missing a main expression after the bindings, hence “bad let”.
(let (a 5) ; should be ((a 5)) (+ a 15))
Again, the interpreter complains about a “bad let”. To understand, let us remember that everything in the first
(...)is taken as a sequence of bindings, taking the form
(variable-name value). By reformatting the
let, the problem is made clear:
(let ( a 5 ) (+ a 15))
5do not have the form
(variable-name value). This is why you need two pairs of parentheses even to define just one varable:
Simplifying code with
let* is a useful tool to make code more readable and understandable. For demonstration purposes, this code is taken from LilyPond, and adapted to contain no
let* at all.
(apply ly:stencil-add (map (lambda (stil accessor) (ly:stencil-translate-axis stil (accessor (coord-translate (interval-widen (ly:relative-group-extent (apply append (map (lambda (g) (cons g (apply append (map (lambda (sym) (cond ((ly:grob? (ly:grob-object g sym)) (list (ly:grob-object g sym))) ((ly:grob-array? (ly:grob-object g sym)) (ly:grob-array->list (ly:grob-object g sym))) (else '()))) (ly:grob-property g 'parenthesis-friends))))) (ly:grob-array->list (ly:grob-object grob 'elements)))) (ly:grob-system grob) X) (ly:grob-property grob 'padding 0.1)) (- (ly:grob-relative-coordinate grob (ly:grob-system grob) X)))) X)) (ly:grob-property grob 'stencils) (list car cdr)))
If you understand nothing in this, you have taken the point. What makes this code hard to read is endless nesting of expressions, which makes you lose track of what is being done, just like if you were reading a single sentence several pages long. Furthermore, the order of execution goes from inner expressions, which are read last, to outer expressions, whereas for us humans it is easier to think when the code executes linearly. Here is the same code rewritten to use
(let* ((elts (ly:grob-array->list (ly:grob-object grob 'elements))) (get-friends (lambda (g) (let* ((syms (ly:grob-property g 'parenthesis-friends)) (get-friends-for-symbol (lambda (sym) (let ((friends (ly:grob-object g sym))) (cond ((ly:grob? friends) (list friends)) ((ly:grob-array? friends) (ly:grob-array->list friends)) (else '()))))) (friend-lists (map get-friends-for-symbol syms)) (friends (apply append friend-lists))) (cons g friends)))) (all-friend-lists (map get-friends elts)) (all-friends (apply append all-friend-lists)) (all-friends-array (ly:grob-list->grob-array all-friends)) (X-refp (ly:grob-common-refpoint-of-array grob all-friends-array X)) (my-X (ly:grob-relative-coordinate grob X-refp X)) (X-ext (ly:relative-group-extent all-friends-array X-refp X)) (padding (ly:grob-property grob 'padding 0.1)) (wide-X-ext (interval-widen X-ext padding)) (parenthesis-positions (coord-translate wide-X-ext (- my-X))) (stencils (ly:grob-property grob 'stencils)) (left-paren (first stencils)) (right-paren (second stencils)) (translated-left-paren (ly:stencil-translate-axis left-paren (interval-start parenthesis-positions) X)) (translated-right-paren (ly:stencil-translate-axis right-paren (interval-end parenthesis-positions) X))) (ly:stencil-add translated-left-paren translated-right-paren))
Without knowing anything about how LilyPond works internally, you can already understand some things: the grobs encompassed by a pair of parentheses are read (
elts), the list is extended so it comprises their “friends” (
all-friends), a horizontal reference point is computed (
X-refp), then a horizontal coordinate (
It is easier to write complicated functions as a big
let*, where variables are bound step-by-step, so that the final expression is simple and short. This advice will be particularly useful while you are a beginner.