There's no way around it: you've got to learn Scheme sometime. Soundscrape is written in Scheme, the interface is in Scheme... but don't worry, it's not hard. For the patient and novitiate, here's a crash course that should only take a few minutes. For the impatient, just skip ahead to Runtime Help and come back if you get lost.
Expressions are the basic units of any language. In English, we have the phrase; in Scheme, we have the sexp, or "Scheme expression." By the end of this chapter, you'll know all about them. Babies learn to gurgle first, so we'll start slow too.
;; to the reader: read "⇒" as "evaluates to"
;; Literals, like strings, are sexps
"foo" ⇒ "foo"
;; Numbers, whether integer, real, complex, or otherwise,
;; are also literals
13 ⇒ 13
;; Anything prefixed with a quote (') is a literal also
'my-symbol ⇒ my-symbol
'(3 2 foo) ⇒ (3 2 foo)
;; Booleans are too
#t ⇒ #t ; (true)
#f ⇒ #f ; (false)Try typing in the expressions you see on the left side at the ss> prompt. The things following the "⇒" should print out in the window.
The language above is too poor to do anything. Let's add some more to it.
Sometimes Scheme is called a functional language due to the important place functions have within the Scheme lexicon. Also called procedures, functions can be created with the lambda operator:
;; A function with two arguments, a and b (lambda (a b) (+ a b)) ⇒ [a new function] + ⇒ [some representation of the + function]
We haven't called the functions yet, though, so they're not being very useful. Let's do that. We can call a function by putting it as the first element of a list.
( ;; lists begin and end with parentheses
(lambda (a b) ;; When it is called, a function returns
(+ a b) ;; the value of its last expression. The
;; last expression here is (+ a b).
3 ;; the first argument (this will be 'a'
;; inside the function)
2 ;; the second argument ('b')
) ;; close the list
⇒ 5 ;; The same as (+ 3 2)
;; More succinctly,
((lambda (a b) (+ a b)) 3 2) ⇒ 5
(+ 3 2) ⇒ 5
;; Procedure (function) calls are Scheme expressions too.
(sin 3) ⇒ 0.141120008059867
;; Procedures are called as a result of evaluating a list.
;; Each member of the list is evaluated before the
;; function is called. That means you can pass in another
;; scheme expression as an argument:
(sin (+ 1 2)) ⇒ 0.141120008059867After gurgling and a few words, babies start to remember stuff. We probably should too. Programming languages use variables to remember things. Identifiers (names) are associated with values using define:
(define x 12) x ⇒ 12 (define y (+ x 3)) y ⇒ 15 (+ x y) ⇒ 27 (define plus (lambda (a b) (+ a b))) (plus x y) ⇒ 27 ;; There's a shortcut for defining functions: (define (plus a b) (+ a b)) ;; plus now has a new definition (plus x y) ⇒ 27
If you already have a variable made and just want to change its value, you can use set!:
(define x 12) x ⇒ 12 (set! x 10) x ⇒ 10
The problem with define is that once you make a variable with it, the variable is around forever. If you want to make a variable, but just have it around for a certain piece of code, you can use let.
;; one pair of parentheses around the whole list of bindings, and one
;; around each individual binding pair
(let ((a 12) ; "let a be 12"
(b 13)) ; "and b be 13"
(+ a b)) ⇒ 25
a ;; (error, a is not defined outside the 'let' block)Having a binding available only within a certain piece of code is called scoping. In the above example, the variables a and b were within scope only within the bounds of the let block. Outside that block, the variables are unknown. Note that when creating a variable binding, whether permanently with define or temporarily with let, the new binding "shadows" any previous binding. We can even redefine +:
(+ 2 4) ⇒ 6 (let ((+ *)) ; "let + be the multiplication operator" (+ 2 4)) ⇒ 8 (+ 2 4) ⇒ 6 ; original binding for + is unaffected
ifTalking and decision making go hand in hand, so we should probably learn something about controlling the program at this stage. Scheme offers a small, but powerful, set of control structures, of which we will only discuss if and "named let". There are quite a few more, so hit the references if you're still thirsty.
The syntax for if is (if TEST EXPRESSION-IF-TRUE EXPRESSION-IF-FALSE). The only thing in Scheme that is false is #f. Everything else is true, including 0, 'abracadabra, '() (the empty list), and anything else you might think of.
(if #t 5 1) ⇒ 5 (if #f 5 1) ⇒ 1 (if 0 5 1) ⇒ 5 (string? 0) ⇒ #f (if (string? 0) "foo" "bar") ⇒ "bar"
Note that only one expression is allowed for EXPRESSION-IF-TRUE and EXPRESSION-IF-FALSE; if you want to put more, you'll have to use begin.
if is a primitive off of which many more conditional constructs can be written, like cond, case, and others. Look these up in the references!
letPerforming one operation on many things is a fundamental operation in computing. In most languages, this is expressed using looping constructs. Although it is possible to loop in Scheme (lookup do in the references), things are usually a bit different. Let's back up a bit and talk more about the structure of lists.
Consider the list (3 4 5 6). The first element, in this case 3, is known (for historical reasons) as the "car" of the list. The rest of the list, in this case (4 5 6), is known as the "cdr" of the list.
(car '(3 4 5 6)) ⇒ 3 (cdr '(3 4 5 6)) ⇒ (4 5 6) (car (cdr '(3 4 5 6))) ⇒ 4 (cdr (cdr '(3 4 5 6))) ⇒ (5 6) ;; Lists can be formed two ways: ;; The list procedure (list 3 4 5 6) ⇒ (3 4 5 6) ;; Or by the cons procedure, which joins a car and a ;; cdr into a new list (cons 3 '(4 5 6)) ⇒ (3 4 5 6)
How does this relate to looping? Well, to get to that we'll have to talk about recursion first. Simply put, recursion is when a function calls itself. The canonical example for recursion is the factorial operation:
;; Remember our shortcut for defining functions? (define (factorial n) (if (zero? n) 1 (* n (factorial (- n 1)))))
You probably know mathematically that N!=N*(N-1)!, where the definition of 0! is 1. Written above is that algorithm, implemented in scheme. The reason that other languages do not normally use recursion to express looping is that for them, it incurs a performance penalty. That's not the case in Scheme -- you can see REFFIXME R5RS:Proper Tail Recursion for more information. So to loop in Scheme, you normally recurse.
One way to recurse is with explicit function definition, as above. Another common way is called named let, which is particularly suited to processing lists.
;; syntax: (let VARIABLE BINDINGS BODY)
;; The BINDINGS are the same as for normal let. VARIABLE will be bound
;; to a function which whose arguments are listed in BINDINGS. When it
;; is called, BODY will be executed again with the arguments of the
;; function used as the BINDINGS.
;;
;; It's easier to show by example.
(let loop ((numbers '(3 -2 1 6 -5)) ;; loop is the new function, and its
(nonneg '()) ;; arguments are numbers, nonneg,
(neg '())) ;; and neg. Initial values are
;; given here.
;; Look up 'cond' in the references!
(cond ((null? numbers) ;; If there are no more numbers to
;; process, return the list of nonnegative
;; and negative numbers.
(list nonneg neg))
((>= (car numbers) 0) ;; If the first element of numbers is
;; nonnegative, loop again, consing that
;; number onto the list of nonnegative
;; numbers.
(loop (cdr numbers) (cons (car numbers) nonneg) neg))
((< (car numbers) 0) ;; Otherwise, loop again, consing the
;; first element onto the list of negative
;; numbers.
(loop (cdr numbers) nonneg (cons (car numbers) neg)))))
⇒ ((6 1 3) (-5 -2))Named let is extremely powerful and a bit confusing at first, so if you don't get it now, come back in a few days. One extension exercise you might want to investigate is to determine for yourself if the above definition for factorial is tail-recursive or not.
This section is really aimed at those that are already familiar with object-oriented programming. If you're not, well... get on my case to write something about it.
SuperCollider is implemented in a dialect of Smalltalk, among the most object-oriented of all languages. It's not an accident that SC succeeded so well; audio programming makes a lot of sense that way. Most things that are possible in SuperCollider are also possible in Soundscrape, although with a Schemey syntax.
Scheme is not, in its canonical form, an object-oriented language. However, Guile provides a rather nice module called GOOPS, the Guile Object-Oriented Programming System. GOOPS extends Guile to be fully object-oriented, complete with multiple inheritance and generic methods with multi-method dispatch.
In most object oriented languages, "knowledge" about how to operate is bound up in objects, and specifically in methods of classes and instances. In GOOPS, the "knowledge" of how to operate is bound up in generic functions. A generic function has a number of methods that implement it. Appropriate methods are chosen (or dispatched) based on the types of the arguments. Let's consider a generic function, length, which returns the length of a list when passed a list, the length of a vector when passed a vector, and 1 when passed anything else.
FIXME: figure out what it is that prevents (define-method (length foo)) from working.
;; Methods are defined with define-method. ;; A method with one argument of type <list>. (define-method (my-length (l <list>)) (length l)) ;; A method with one argument of type <vector>. (define-method (my-length (v <vector>)) (vector-length v)) ;; A method for everything else. (define-method (my-length obj) 1) (my-length '(3 4 5 6)) ⇒ 4 ;; '#' means a vector (my-length #(3 4 5 6)) ⇒ 4 (my-length 17) ⇒ 1