Skip to content

Local Binding with let

The basic form for local bindings is let, which we'll explore in most detail. The companion procedures let* and letrec can then be shown quite concisely. As an example we'll implement a color damper that damps the RGB components of a color by a given factor. In order to benefit from color highlighting and easier editing we will do that in LilyPond files and not on the Scheme REPL.

The typical use case for let expressions is within procedures where we would get the original data from the procedure arguments. But for the sake of our example we simply create a global variable that we can then reference:

% Create a pair with a color and a damping factor
#(define props (cons yellow 1.3))

props is now ((1.0 1.0 0.0) . 1.3), a pair with a list for the yellow color and a number for the damping factor. What we need to do with it is pretty simple: just create a new list with three elements where each element is the corresponding element from the car of props divided by the factor stored in the cdr of props. In order to later display the value (and to practice) we bind the resulting list to a top-level variable defined in LilyPond syntax:

dampedYellow =
#(list
  (/ (first (car props)) (cdr props))
  (/ (second (car props)) (cdr props))
  (/ (third (car props)) (cdr props)))

#(display dampedYellow)

which will print (0.769230769230769 0.769230769230769 0.0) to the console. (You could also use that variable to override a color property of a grob, by the way.)

So what's the problem with that? It's the redundancy of the calls to car and cdr, three times for each. Each time we need the color or the damping factor we have to access the original variable and extract the data from it. In the case of car and cdr this is not a real issue but in real-world code one will have expressions that are considerably more complicated to write and computationally more expensive.

The solution is to create local bindings of the values of (car props) and (cdr props) and then refer to these bindings within the body of the expression. This is done with the let expression that takes the general form

(let (<bindings>) exp1 exp2 ...)

or, displayed in a more hierarchical manner:

(let
  (
    <binding 1>
    <binding 2>
    ...
  )
  <expression 1>
  <expression 2>
  ...
)

The let expression has two parts, the bindings and the body, where each part must have at least one entry. The bindings are enclosed by parens as a whole, and each binding has the form (name value) where name is a symbol (the “name” of the local binding) and value any Scheme value. While the value could be of any type including literal values the whole point of it is to bind the results of complex expressions to simple names.

In our example we create two bindings, one for the color and one for the damping factors, which looks like this:

(let
  (
    (color (car props))
    (damping (cdr props))
  )
  <expression 1>
  <expression 2>
  ...
)

Now we have two “local variables”, color and damping that are visible and accessible from within the body of the let expression.

This body of a let expression is an arbitrary number of expressions that is evaluated in sequence. Note that - different from the bindings - there is no extra layer of parens around the body. The value of the final expression will become the value of the let expression as a whole. In our example we only have one expression, the list creation. But instead of repeatedly calling car and cdr we can now use the local variables directly.

As this list expression is the only (and therefore last) in the body the whole let expression evaluates to the created list, and this value (with the damped color) is bound to the top-level variable:

dampedYellowWithLet =
#(let
    (
      (color (car props))
      (damping (cdr props))
    )
    (list
      (/ (first color) damping)
      (/ (second color) damping)
      (/ (third color) damping)
    )
 )

#(display dampedYellowWithLet)

which will print the same result as before.

The indentation of the previous example was pretty unidiomatic and intended to exemplify how we constructed the expression step by step. Usually one would use a more condensed way, for example

dampedYellowWithLet =
#(let ((color (car props))
       (damping (cdr props)))
   (list
    (/ (first color) damping)
    (/ (second color) damping)
    (/ (third color) damping)))

You may think that this expression is considerably more complex than our initial, direct application of list. But as mentioned the car and cdr expressions are comparably simple, and the “complexity ratio” will change dramatically with more complex expressions.

The nesting and parenthesizing levels in that code look daunting, but indeed they are the result of a completely consequent structure, and (in theory) there's no room for misunderstandings. However, the frustrating truth is that in practice (for the beginner) it is a miracle how and where to place the parens, and the error messages aren't really encouraging, to say the least. Therefore I have written a dedicated chapter discussing the typical error conditions we tend to produce.


Last update: November 3, 2022