Local variables#
let
syntax#
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 define
s are complex, have 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.
This is why define
is only useful at the top level, to define functions, or
constants such as
(define PI 3.1415926535)
For temporary variables which are no longer needed 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
...)
When 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 let
expression.
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.
A 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 number-of-balls
and 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.
Unlike let
, 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 always want to use let*
.
Parenthesizing a let
expression#
let*
expressions contain many parentheses. It is easy for the inexperienced 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 expression 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 top level.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. Your 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
is constructed:(let (xxxxxxxxx xxxxxxxxx) xxxxxxx xxxxxxx)
The first
(...)
contains all bindings. Here, this(...)
actually contains everything that’s inside thelet
expression. 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. Thelet
is also missing a main expression after the bindings, hence “bad let”.Omitting parentheses.
(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 thelet
, the problem is made clear:(let ( a 5 ) (+ a 15))
Indeed,
a
and5
do not have the form(variable-name value)
. This is why you need two pairs of parentheses even to define just one variable:((a 5))
.
Simplifying code with let*
#
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
and let*
:
(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 (my-X
), etc.
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.