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")
      (define birth 1756)
      (format #t
              "Mozart was born in ~a, his 300th birthday will be in ~a."
              (+ birth 300))))

(NB: At least, this program will not work with Guile 2.2. The rules over the placement of defines 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)

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)))
     (if (and (= x 1)
              (= y 1))
         "You lost!"))))

 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

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)))
     (if (equal? ball 0)
         "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)))
     (if (equal? ball 0)
         "You lost."))))

 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

    The first (...) contains all bindings. Here, this (...) actually contains everything that’s inside the let 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. The let 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 the let, the problem is made clear:

    (let (
      (+ a 15))

    Indeed, a and 5 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
         (lambda (stil accessor)
                     (apply append
                              (lambda (g)
                                (cons g
                                      (apply append
                                               (lambda (sym)
                                                    ((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)))
                                               (ly:grob-property g 'parenthesis-friends)))))
                                (ly:grob-object grob 'elements))))
                     (ly:grob-system grob)
                   (ly:grob-property grob 'padding 0.1))
                (- (ly:grob-relative-coordinate grob
                                                (ly:grob-system grob)
         (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)))
         (lambda (g)
           (let* ((syms (ly:grob-property g 'parenthesis-friends))
                    (lambda (sym)
                      (let ((friends (ly:grob-object g sym)))
                          ((ly:grob? friends)
                           (list friends))
                          ((ly:grob-array? friends)
                           (ly:grob-array->list friends))
                  (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))
         (ly:stencil-translate-axis left-paren (interval-start parenthesis-positions) X))
         (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.