One of a series of tutorials about Scheme in general
and the Wraith Scheme interpreter in particular.
Copyright © 2011
Jay Reynolds Freeman,
all rights reserved.
Personal Web Site: http://JayReynoldsFreeman.com
The kind of procedure creation that I will discuss in this tutorial is mostly syntactic sugar that is built into the "define" special form, but I am going to show you the frosting without the cake. Scheme procedures are intimately involved with lambda expressions -- which we have not yet discussed -- and lambda expressions are by and large the heart and soul of Scheme, but you don't need to know about them to create your own procedures. So we will stick with the sugar-coated version of the syntax.
We have already seen a few examples of procedures that are not built into Scheme. Let's look at one of them.
(define (increment n) (+ n 1) )
The example creates a procedure named "increment". The text shown is syntactic sugar for
(define increment (lambda (n) (+ n 1) ) )
but this tutorial is not about lambda expressions: I showed that one to illustrate clearly that the syntactic sugar is defining something named "increment", with the same pattern for the use of "define" that we have already encountered. The symbol "increment" has a Scheme object bound to it, only the Scheme object is more complicated than the numbers, strings or lists that I have used in most of the examples in preceding tutorials.
The syntactic sugar works as follows. First there is the "define", and then a list whose first item is the name of the procedure. The rest of the items of the list are the names of the arguments of the procedure. There don't need to be any arguments at all -- it is all right to have a procedure that has no arguments. Or there can be more than one. There is also a feature of the syntactic sugar that allows you to write a procedure that can be called with different numbers of arguments at different times: I will show you how later.
Nothing in the name-and-argument list is evaluated. The contents of that list are all names for things.
Everything between the end of the name-and-argument list and the end of the procedure definition is the body of the procedure. It is the actual Scheme code that does the work of the procedure. There is only one expression in this procedure, "(+ n 1)": It adds one to the procedure's argument, "n". Procedures return whatever the last expression that they evaluate does, so the example procedure returns a number which is one greater than the argument with which it was called.
After we have evaluated the definition, we can use "increment" just like any other procedure, as part of a procedure application.
(define (increment n) (+ n 1) ) ;; ==> increment (increment 41) ;; ==> 42 (increment (increment 1)) ;; ==> 3
Here is a procedure with no arguments.
(define (greetings) (display "Hello, world!\n")) ;; ==> greetings (greetings) ;; ==> Hello, world! ;; #t
Here is one with two arguments, to calculate the hypotenuse of a right triangle.
(define (hypotenuse side-a side-b) (sqrt (+ (* side-a side-a) (* side-b side-b)))) ;; ==> hypotenuse (hypotenuse 3 4) ;; ==> 5 (hypotenuse (+ 3 5) (* 5 3)) ;; ==> 17
You can create procedures that take any number of arguments in the same way.
By the way, don't get the idea that you have to write out procedure definitions on multiple lines with neat indentation. I am doing that for the sake of clarity. Scheme doesn't care. You could equally well have written it all on one line
(define (hypotenuse side-a side-b) (sqrt (+ (* side-a side-a) (* side-b side-b)))) ;; ==> hypotenuse
or with any combination of line breaks and indentation you like.
(define (hypotenuse side-a side-b) (sqrt ( + (* side-a side-a) (* side-b side-b)))) ;; ==> hypotenuse
I recommend that you choose a style of writing code that you find easy to read, but it is not for me to tell you which one.
The bodies of the procedures defined above all contain only one Scheme expression, but more than one expression is allowed. Since a procedure returns the last thing it evaluates, any other expressions will likely matter only because of side effects -- perhaps they will print something out, or change the value bound to a variable. Here is an example that changes the value of one of its arguments and returns the changed value, and that uses "display" statements within the body of the procedure to describe what it is doing.
(define (change-x-and-report x) (display "In the procedure, x starts as ") (display x) (display ".\n") (set! x 42) (display "In the procedure, x has been changed to ") (display x) (display ".\n") x) ;; ==> change-x-and-report (change-x-and-report 17) ;; ==> In the procedure, x starts as 17. In the procedure, x has been changed to 42. 42
Incidentally, we could have written "change-and-report" with fewer "display"s, by using "string-append", perhaps like this.
(define (change-x-and-report x) (display (string-append "In the procedure, x starts as " (number->string x) ".\n") (set! x 42) (display (string-append "In the procedure, x has been changed to " (number->string x) ".\n") x) ;; ==> change-x-and-report (change-x-and-report 17) ;; ==> In the procedure, x starts as 17. In the procedure, x has been changed to 42. 42
That version of "change-x-and-report" is longer, but some people would find it easier to read. You decide which style works for you.
Now let me describe how to write a procedure that can be called with one or more arguments. The syntactic sugar for this is to write the stuff inside the name-and-arguments list with the procedure name first -- as we have seen before -- and then with two variable names separated by a ".", as if the whole name-and-arguments structure were a series of cons cells that ended with a dotted pair. That's not really what it is; the "." is just to remind you that Scheme is going to treat the actual arguments as if it were. In the next example, I have used the "first-arg" and "all-the-rest" for the variable names, but you can use any names you like.
(I mention that explicitly because it is almost a tradition to use the names "first" and "rest" where I have used "first-arg" and "all-the-rest". If I did that and you also saw it in other Scheme documents, you might think that those names were required. They aren't.)
Note that I haven't yet told you what Scheme does with this syntactic sugar. I am going to give the example first and then explain it.
(define (make-a-list first-arg . all-the-rest) (append (list first-arg) all-the-rest)) ;; ==> make-a-list (make-a-list 1) ;; ==> (1) (make-a-list 1 2) ;; ==> (1 2) (make-a-list 1 2 3) ;; ==> (1 2 3) (make-a-list 1 2 3 4) ;; ==> (1 2 3 4)
The use of "append" in the body of the procedure, and the results, should pretty well indicate what is happening. Can you figure it out?
What Scheme does is take the first argument to the procedure -- which was "1" in all of the preceding procedure applications -- evaluate it, and bind it to the first variable name in the procedure definition -- which was "first-arg". Scheme then evaluates all the remaining arguments and makes a list out of them, and binds that entire list to the second variable name in the procedure definition. In the first procedure application using "make-a-list", there were no remaining arguments, so that what got bound to "all-the-rest" was the empty list. In each of the other procedure applications, what was bound to "all-the-rest" was a list of numbers beginning with "2". So all "make-a-list" had to do to return a list of all of its arguments was to make a list explicitly containing the first argument, and then append the second argument to it.
You can also see why the "." in the syntactic sugar is a good reminder of what is going on, and why the names "first" and "rest" are almost traditional. Consider one of those procedure applications as a list -- forget for a moment the fact that Scheme will start evaluating things the moment you type it in, just look at it.
(make-a-list 1 2 3 4)
Recall that we can always use dotted-pair notation -- with "."s -- in writing out lists. We don't need to put in all the dots; we can just put them in where we wish to be very explicit about the fact that lists are built up of connected cons cells. In particular, one way to write this list with "."s is
(make-a-list 1 . (2 3 4))
If you don't believe me, try typing that expression into Wraith Scheme with a quote in front of it.
'(make-a-list 1 . (2 3 4)) ;; ==> (make-a-list 1 2 3 4)
Scheme doesn't use the dotted-pair notation when writing out lists unless it absolutely has to. So when "quote" does its assigned task of returning the quoted object without evaluation, it returned the same list you typed in, only with no dots, and in that form you can see that it is precisely what I said it was.
Now compare the following two expressions -- again unevaluated -- and I have inserted some extra white space to make the comparison easier.
(make-a-list 1 . (2 3 4) ) (make-a-list first-arg . all-the-rest )
This form makes it easy to see that the syntactic sugar is reminding you that the first argument gets bound to the first variable name, and a list of all the rest of the arguments gets bound to the second variable name. Furthermore, it is clear that the semi-traditional variable names "first" and "rest" would be briefer and equally appropriate to characterize what is going on.
There is even a way to write a procedure that takes zero or more arguments.
(define (make-a-possibly-empty-list . all-the-args) all-the-args) ;; ==> make-a-possibly-empty-list (make-a-possibly-empty-list) ;; ==> () (make-a-possibly-empty-list 1) ;; ==> (1) (make-a-possibly-empty-list 1 2) ;; ==> (1 2) (make-a-possibly-empty-list 1 2 3) ;; ==> (1 2 3) (make-a-possibly-empty-list 1 2 3 4) ;; ==> (1 2 3 4)
In this case, the syntactic sugar merely bound all the evaluated arguments -- if any -- to "all-the-args".
You can put a "dotted arg" as the last item in a name-and-arguments list of any length. The following procedure doesn't do anything with its arguments, but it does illustrate the general principle.
(define (foo a b c d e f g h . all-the-rest) #t) ;; ==> foo
I myself do not use Scheme procedures with variable numbers of arguments very often. Yet sometimes they are invaluable.
-- Jay Reynolds Freeman (Jay_Reynolds_Freeman@mac.com)