This is from the SICP book that I am sure many of you are familiar with. This is an early example in the book, but I feel an extremely important concept that I am just not able to get my head around yet. Here it is:
(define (cons x y)
(define (dispatch m)
(cond ((= m 0) x)
((= m 1) y)
(else (error "Argument not 0 or 1 - CONS" m))))
dispatch)
(define (car z) (z 0))
(define (cdr z) (z 1))
So here I understand that car
and cdr
are being defined within the scope of cons
, and I get that they map some argument z
to 1 and 0 respectively (argument z
being some cons
). But say I call (cons 3 4)
...how are the arguments 3 and 4 evaluated, when we immediately go into this inner-procedure dispatch
which takes some argument m
that we have not specified yet? And, maybe more importantly, what is the point of returning 'dispatch
? I don't really get that part at all. Any help is appreciated, thanks!
Scheme procedures are first-class objects in the language; you refer to a procedure in the same way you refer to any other object, via a pointer. A "procedure name" is really just a variable name, and you can do the same things with "procedure" variables as with any other variable.
Scheme has a special form that is very special, called lambda . It creates a first-class procedure and returns a pointer to it.
A function that takes another function as one of its arguments, as every does, is called a higher-order function. If we focus our attention on procedures, the mechanism through which Scheme computes functions, we think of every as a procedure that takes another procedure as an argument—a higher-order procedure.
A function call is written as (f args) where f is the name of the function and args a space-separated sequence of arguments. So to call tab without arguments, you'd write (tab) and to call translate with the argument x , you'd write (translate x) .
This is one of the weirder (and possibly one of the more wonderful) examples of exploiting first-class functions in Scheme. Something similar is also in the Little Schemer, which is where I first saw it, and I remember scratching my head for days over it. Let me see if I can explain it in a way that makes sense, but I apologize if it's not clear.
I assume you understand the primitives cons
, car
, and cdr
as they are implemented in Scheme already, but just to remind you: cons
constructs a pair, car
selects the first component of the pair and returns it, and cdr
selects the second component and returns it. Here's a simple example of using these functions:
> (cons 1 2)
(1 . 2)
> (car (cons 1 2))
1
> (cdr (cons 1 2))
2
The version of cons
, car
, and cdr
that you've pasted should behave exactly the same way. I'll try to show you how.
First of all, car
and cdr
are not defined within the scope of cons
. In your snippet of code, all three (cons
, car
, and cdr
) are defined at the top-level. The function dispatch
is the only one that is defined inside cons
.
The function cons
takes two arguments and returns a function of one argument. What's important about this is that those two arguments are visible to the inner function dispatch
, which is what is being returned. I'll get to that in a moment.
As I said in my reminder, cons
constructs a pair. This version of cons
should do the same thing, but instead it's returning a function! That's ok, we don't really care how the pair is implemented or laid out in memory, so long as we can get at the first and second components.
So with this new function-based pair, we need to be able to call car
and pass the pair as an argument, and get the first component. In the definition of car
, this argument is called z
. If you were to execute the same REPL session I had above with these new cons
,car
, and cdr
functions, the argument z
in car
will be bound to the function-based pair, which is what cons
returns, which is dispatch
. It's confusing, but just think it through carefully and you'll see.
Based on the implementation of car
, it appears to be that it take a function of one argument, and applies it to the number 0
. So it's applying dispatch
to 0
, and as you can see from the definition of dispatch
, that's what we want. The cond
inside there compares m
with 0
and 1
and returns either x
or y
. In this case, it returns x
, which is the first argument to cons
, in other words the first component of the pair! So car
selects the first component, just as the normal primitive does in Scheme.
If you follow this same logic for cdr
, you'll see that it behaves almost the same way, but returns the second argument to cons
, y
, which is the second component of the pair.
There are a couple of things that might help you understand this better. One is to go back to the description of the substitution model of evaluation in Chapter 1. If you carefully and meticulously follow that substitution model for some very simple example of using these functions, you'll see that they work.
Another way, which is less tedious, is to try playing with the dispatch
function directly at the REPL. Below, the variable p
is defined to refer to the dispatch
function returned by cons
.
> (define p (cons 1 2))
#<function> ;; what the REPL prints here will be implementation specific
> (p 0)
1
> (p 1)
2
The code in the question shows how to redefine the primitive procedure cons
that creates a cons-cell (a pair of two elements: the car and the cdr), using only closures and message-dispatching.
The dispatch
procedure acts as a selector for the arguments passed to cons
: x
and y
. If the message 0
is received, then the first argument of cons
is returned (the car
of the cell). Likewise, if 1
is received, then the second argument of cons
is returned (the cdr
of the cell). Both arguments are stored inside the closure defined implicitly for the dispatch
procedure, a closure that captures x
and y
and is returned as the product of invoking this procedural implementation of cons
.
The next redefinitions of car
and cdr
build on this: car
is implemented as a procedure that passes 0
to a closure as returned in the above definition, and cdr
is implemented as a procedure that passes 1
to the closure, in each case ultimately returning the original value that was passed as x
and y
respectively.
The really nice part of this example is that it shows that the cons-cell, the most basic unit of data in a Lisp system can be defined as a procedure, therefore blurring the distinction between data and procedure.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With