Expressions#
Technically speaking, Scheme code does not revolve around instructions but expressions. Let’s see what this means in practice. Rather than giving instructions to the machine (do this, then that), you need to explain how to construct the result you want by combining values with operations.
First operations#
Let us come back on an example that you already saw earlier:
(+ 42 5)
This syntax is absolutely fundamental to Scheme. It is the syntax of a function call, or, using a term that is more common in the Scheme world, the call of a procedure. LilyPond also has functions. They are called like this:
\function argument1 argument2 ...
On the other hand, in Scheme, this construct is written like this:
(function argument1 argument2 ...)
Beware, parentheses are not optional!
(+ 2 2)
⇒ 4
+ 2 2
⇒ #<primitive-generic +>
⇒ 2
⇒ 2
Indeed, + 2 2
unparenthesized is just some expressions in a row, without any
link between them. It is as if you have typed them one by one and waited for the
result every time like this:
+
⇒ #<primitive-generic +>
2
⇒ 2
2
⇒ 2
In Scheme, the role of parentheses is not just to group elements. They indicate that the first element is a procedure (i.e., a function), which is being applied to the remaining elements. On the other hand, do not write too many parentheses either!
((+ 2 2))
⇒ ERROR: Wrong type to apply: 4
ABORT: (misc-error)
Why this error message? Let us decompose what happens. First, the inner
expression inside the parentheses is evaluated. The computation (+ 2 2)
evaluated to 4. The expression becomes:
(4)
You can see that as:
(4
; There could be more elements here.
)
Thus, (4)
is trying to call the function 4 without arguments. However, 4 is
not a function! This is why you get the error “Wrong type to apply: 4”. The
value 4 does not have a type that can be called like a function.
Other simple operations are done in a similar fashion:
(+ 2 2) ; 2 plus 2
(- 5 3) ; 5 minus 3
(* 5 6) ; 5 times 6
(/ 6 4) ; 6 divided by 4
If you are unfamiliar with this prefix notation for what is usually written \(2 + 2\), \(5 - 3\), \(5 \cdot 6\) and \(6 / 4\), it will likely take a while to get used to it. You need to reframe your mind to think in this Yoda-style order. Instead of “two plus two”, you need to say “the sum of two and two”. Similarly, instead of “5 times 6”, Scheme says “the product of 5 by 6”.
By the way, note that the division operation on integers always gives an exact,
if fractional, result. Thus, (/ 6 4)
gets simplified and evaluates as 3/2
(Scheme’s notation for the fraction three halves).
These four functions also accept more than two arguments.
(+ 3 4 5)
In typical other languages, you would have needed to repeat the +
operator
twice, writing 3+4+5
. In Scheme, this is unnecessary due to the way
parentheses delimit the arguments. (+ 3 4 5)
is simply the sum of 3, 4 and 5,
just like (+ 3 4)
is the sum of 3 and 4.
This function call syntax is so ubiquitous that you already saw it in some of the previous examples:
(display "some string")
(skip-of-length #{ { c'8 d' e' f' e' d' } #})
display
and skip-of-length
are also procedures, which are called in the same
way as +
, etc.
While we are at arithmetic procedures, there are two useful predefined
procedures called 1+
and 1-
. These add and subtract 1, respectively. (They
are only predefined procedures, not a notation, you can’t write 2-
for
example.)
(1+ 5) ⇒ 6 ; equivalent to (+ 5 1)
(1- 5) ⇒ 4 ; equivalent to (- 5 1)
Finally, let us turn our attention to this:
+
⇒ #<primitive-generic +>
Scheme procedures are values like any other value. This representation with
pointy brackets is what the interpreter gives because it doesn’t have much
better to do. Indeed, it is hard to imagine how to write the “value” of the
procedure. Yet, the procedure exists as a proper value. Just like 4
is an
integer, 42.5
is a floating-point number, and "abcd"
is a string, +
is a
procedure. You may ask, what is the nature of this “procedure” object? Actually,
what an object is is not as important to know in programming as what the
object does. Take numbers. You can print them, add them, subtract them,
multiply them, etc. Procedures have just one operation to offer, procedure call.
A procedure is simply an object that can be called, which is done with the
syntax (procedure argument1 argument2 ...)
.
Also, just like all values, it is possible to store procedures in variables. +
is really just a predefined variable containing a frequently used procedure. You
could do:
(define sum +)
(sum 2 2)
⇒ 4
Nesting expressions#
With these considerations in mind, let us try to write a slightly more complex expression:
Here is the mental path to translating this expression in Scheme. You need to start by wondering what the type of operation is. This is an addition:
(+ ... ...)
Now, complete the holes: this is the sum “of what?” The sum of \(2 \times 3\) and \(3 \times 5\). Now continue in the same way. \(2 \times 3\) is the product of 2 by 3, and \(3 \times 5\) is the product of 3 by 5. Therefore, this is how this computation is expressed in Scheme:
(+ (* 2 3) (* 3 5))
Now that you know how to nest expressions in expressions, try to use the sandbox to compute the value of
(you should find -149).
Formatting code#
As you have seen, programming in Scheme is essentially nesting expressions in larger expressions. If you did the exercise above, you probably noticed that there were four successive closing parentheses at the end. In more complex programs, it is not unusual to close ten parentheses in a row. The name of Lisp, the family of languages to which Scheme belongs, means “List Processing”. However, it is turned by many into the joke “Lots of Insipid Stupid Parentheses”. Beginners may have a mental image of Scheme that resembles this:
()(((((()))))(( )((())((()( )()) )(() ())()((()((((( ())( ()() ())()(()(())((
)))((())((())))( (()())((())) )))) )(() ))()()(()(()( ()((() ))((( ((()()(())(()
))()))(()))() (())((())()( ))() ())( (()) )) )(( (())
)))))((((()(( ))()()()))())) )(() )(() ()(( )(( ( ()( (())
)()(((()))(()( )()(()()()()()( ((() ()() )()) ))) ) ()( ()))
)()(()()((())( (()()(((())))) ()))())))(((()()( ))))())(( ))( ((( ((((()())
))(((())(()(( (())((()))(())) )(((((()(((()())( )()) )() ))) )))(
)()))()()() ))()))))((())) ()() (()) ()() ()( )(( (())
)(((())(()) )))()()((()(()( )))( ))(( ))(( ()) )() ()((
))((((()())) )(()(()()))) ()(( ()(( )()( ((( ))( (()(
()()))((()()() ((()())))))()))) ()() ))() ()()()(())() ((( )() ())()(()())(
((())))(((())(() )(()()((())((( ()() )()( )))))())((()(( )() ()( ())(()(())))))
The best way to keep your head above the water is to adhere to certain simple rules for formatting your code in order to keep it readable. There are three possible styles for indenting an expression. The first style is putting it all on one line.
(procedure argument1 argument2 ...)
The second style is to write each new argument on a new line. Arguments are indented by one space relative to the open parenthesis, so they are aligned with the procedure name.
(procedure
argument1
argument2
...)
Finally, the third style places the first argument on the same line as the procedure. The remaining arguments follow, each on their own line, indented the same as the first.
(procedure argument1
argument2
...)
In all cases, remember these two facts: arguments of a procedure are always aligned, and you never put parentheses alone on their own line! A human reader understands the code from its layout, without ever needing to count parentheses. Here is one way to format the solution of the previous exercise, mixing the three styles:
(-
1
(* 5 6)
(* 3 (+ 4
(* 3 12))))
A Scheme programmer reading this code immediately notices that three arguments
are being given to -
, and sees where they begin and end. Again, no counting of
parentheses is involved.
Finally, there is a rule that is not respected by all programmers, but nevertheless useful to follow: “closing paren closes the line”. As long as you have a closing paren, start a new line. For example, rather than:
(+ (* 3 4) 5)
it is better to write:
(+ (* 3 4)
5)
or:
(+
(* 3 4)
5)
or, swapping arguments to put the more complex last:
(+ 5 (* 3 4))
Sequences of expressions#
In a LilyPond file, a hash introduces one single expression. If you need several expressions, for example to define several variables or to display debugging messages, you need one hash for each of the expressions.
#(display "Hello ")
#(define surname "Jane")
#(display surname)
There is another possibility, which is to enclose all expressions in
(begin ...)
.
#(begin
(display "Hello ")
(define surname "Jane")
(display surname))
In a begin
, there can be any number of expressions. They are evaluated first
to last, and the value of the last expression becomes the value of the whole
begin
.
(begin
(display "Hello ")
(define surname "Jane")
(display surname)
(display "!\n")
surname)
⊨ Hello Jane!
⇒ "Jane"