Like Lisp, Scheme is a functional language with some procedural elements. Therefore, we start our investigation with a number of expressions entered at the interactive MzScheme prompt.
> "Hello World" "Hello World" > (display "Hello World\n") Hello World > (+ 4 5 6) 15
The interactive shell evaluates the expression and displays the result (in case there is any). The first expression is a string constant which evaluates to itself. The result is shown with the surrounding double quotes indicating the string constant.
The second expression is a function call that writes our message to standard output. Scheme inherits its syntax from Lisp. An expression is either a literal or a list of expressions enclosed in parentheses. And as in Lisp, these S-expressions are evaluated by interpreting the first expression (the head of the list) as a prefix operator to be applied to the remaining expressions of the list (its tail). In the example, the built-in display function is applied to our message string. In contrast to the first example, the output is the result of the side effect of the function call. The result of the expression is empty and therefore not displayed.
The third example exemplifies one of the advantages of the simple but powerful S-expression syntax: many functions (such as the arithmetic operators) can be applied to an arbitrary number of arguments.
Scheme has a well designed set of numerical types including arbitary long integers, rationals, floating point numbers. We can also construct complex numbers on top of these types.
> (exp 1) 2.718281828459045 > (expt 2 4) 16 > (expt 2 200) 1606938044258990275541962092341162602522202993782792835301376 > (sqrt -1) 0+1i > (+ 1/3 1/2) 5/6 > (/ 5 2) 5/2 > (quotient 5 2) 2 > (remainder 5 2) 1 > (* 1/2 -3+2i) -3/2+1i > (* 1.5 -3+2i) -4.5+3.0i
Note how the use of the operators -, +, and / as part of the number literals lets us express all these different types in a natural way.
Scheme always tries to find the "best" type for the result of an expression. Dividing two integers, for example, results in a rational. Multiplying a complex integer with a rational, we obtain a complex rational number. Only if an operand is a floating point number or the expression results in an irrational number such as exp 1, the result is a floating point (and therefore inexact) number.
Boolean expression work as expected (once we get used to the prefix notation) with #t and #f denoting the boolean literals true and false.
> (and (> 2 1) (< 1.5 2.0)) #t > (not #t) #f
The Scheme standard also guarantees a number of useful string and character functions. Character literals start with #\ followed by the character itself.
> (string-length "blah") 4 > (string-ref "blah" 2) #\a > (string=? "blah" "blub") #f > (string<? "blah" "Blub") #f > (string-ci<? "blah" "Blub") #t > (string->number "123") 123 > (string->number "100" 16) 256 > (string-append "blah" "blub") "blahblub" > (substring "blah" 1 3) "la"
Two things are worth pointing out. First, the use of the question mark and the arrow -> as part of the function name to indicate the meaning of the function: Predicates such as the string comparisons always end with a question mark, and conversion functions use the arrow to denote what is being converted. Second, the indexing of strings (and all other sequences): indexes start with zero and use the half-open interval semantics just like Python (ok, Scheme was first), that is, (substring "blah" 1 3) is equivalent to Python's "blah"[1:3].
In case you have not guessed it already: the string comparison suffix -ci stands for "case insensitive".
We can bind a value to a symbol in the global environment with the define command.
> (define x 1.5) > (* 2 x) 3.0
define is an example of a structure (also called "special form" like in Common Lisp) which looks like a function, but is actually implemented directly by the Scheme interpreter, because the functionality can not be expressed by a normal Scheme function.
Scheme has three different kinds of conditional expressions: if, cond, and case. The if expression chooses one of two alternatives depending on a condition, cond generalized this to multiple conditions and alternatives, and the case selects an alternative by checking if the discrimator is contained in the associated list.
> (define x 3) > (if (< x 10) "small" "big") "small" > (cond ((< x 0) "negative") ((= x 0) "zero") (else "positive")) "positive" > (case x ((1 2) "small") ((3 4) "medium") (else "big")) "medium"
Naturally, functions play a central role in a functional language, and Scheme really treats them as first class citizens. In contrast to Common Lisp, Scheme is a single-cell Lisp dialect, that is, a symbol is bound to a value which may be an expression or a function.
One way to define a function is to bind a symbol to a lambda expression (an anonymous function).
> (define times2 (lambda (x) (* 2 x))) > (times2 55) 110
lambda is a special form which takes a formal parameter list and an expression and returns the associated function object.
As a shortcut, Scheme allows us to define the function directly using define followed by the signature (name and arguments) and expression of the function.
> (define (times2 x) (* 2 x)) > (times2 55) 110
Of course, function definitions can be recursive.
> (define (fac n) (if (< n 2) 1 (* n (fac (- n 1))))) > (fac 5) 120
We can also define functions with variable argument lists. Normally, the arguments passed to a function are bound to the formal parameters defined in the function definition one by one. We can accept an arbitrary number of arguments by separating the last formal parameter with a period. When the function is called, the arguments which can not be bound to the "normal" formal parameters (before the period) will be bound as a list to this additional formal parameter.
> (define (show-args first-arg . more-args) (display first-arg) (newline) (display more-args) (newline)) > (show-args 1 2 3 4) 1 (2 3 4)
The show-args function also demonstrates that a function definition can consist of multiple expressions which will be evaluated in sequence and the result of the last expression will be returned as the result of the function.
> (define (blah) 1 2 3) > (blah) 3
Since functions are treated just like any other value, defining higher order functions (also called "meta procedures" in Scheme) is straight forward.
> (define (compose f g) (lambda args (f (apply g args)))) > ((compose times2 times2) 4) 16
Here we use the built-in apply function which applies (surprise) a function to a list of arguments.
There are three built-in collection types: lists, vectors, and pairs. Lists we have used already throughout this chapter since they are not only a collection type, but also the base of Scheme's expression syntax. Vectors are similar to lists, but optimized for random access. Pairs combine two values and are also the building block of lists.
Pairs follow the Lisp tradition: they are constructed with the cons function, and can be taken apart with the car (head), and cdr (tail) function.
> (define p (cons "a" "b")) > (car p) "a" > (cdr p) "b"
As we have seen, lists literals are sequences of expressions separated by white space and enclosed in parentheses. Since they are by default evaluated as expressions, we have to quote them to obtain the lists as such.
> '(1 2 3) (1 2 3) > '("a" \#b 37) ("a" |#b| 37)
Internally, lists are nested pairs, or more precisely, a list is either empty (()) or a pair whose second element is a list. Therefore, the list syntax is just a shortcut for a nested pair structure and we can apply the pair functions to obtain the head and tail of a list.
> (cons 1 ()) (1) > (cons 1 (cons 2 ())) (1 2) > (cons "a" '("b" "c")) ("a" "b" "c") > (car '(1 2 3)) 1 > (cdr '(1 2 3)) (2 3)
As a generalization, we can combine the letters a and d between c and r to retrieve the n-th element at the beginning or end of the list.
> (cadr '(1 2 3 4 5)) 2 > (caddr '(1 2 3 4 5)) 3 > (caar '((1 2 3) 4 5)) 1 > (cdar '((1 2 3) 4 5)) (2 3) > (cadar '((1 2 3) 4 5)) 2
Besides these primitive list functions, we are a number of useful list functions in the standard.
> (length '(1 2 3)) 3 > (reverse '(1 2 3)) (3 2 1) > (append '(1 2) '(3 4)) (1 2 3 4) > (list-ref '(1 2 3 4) 2) 3 > (member "a" '(1 "a" 5.5)) ("a" 5.5) > (member "b" '(1 "a" 5.5)) #f
Note that the member function returns the sublist starting with the element we look for or false if it is not contained in the list.
As an example of a higher order function, we have a look at the map function which applies a function to each element in a list and returns the list of the results.
> (map times2 '(1 2 3)) (2 4 6) > (map (lambda (x) (* 2 x)) '(1 2 3)) (2 4 6)
Here we appreciate again the simple handling of functions as values. The standard does not define many higher order list functions, but most Scheme implementation come with a library containing the usual functions we have met with in the previous chapters. To use these functions with MzScheme, we first have to load the list library list.ss.
> (require (lib "list.ss")) > (filter odd? '(1 2 3 4 5)) (1 3 5) > (foldl + 10 '(1 2 3 4)) 20 > (quicksort '(5 3 7 2 4 1 8) <) (1 2 3 4 5 7 8)
Vector literals look like lists following a # sign. Their main characteristic is the efficient access to individual elements by index using the vector-ref and vector-set! functions.
> #(1 2 3 4) #4(1 2 3 4) > (define v '#(1 2 3 4)) > (vector-length v) 4 > (vector-ref v 2) 3 > (vector-set! v 2 55) > v #4(1 2 55 4)