In this post I will quickly explain the basic principle of SchemeTalk: how closures can be turned into objects. While this topic is well-known, I will explain it shortly anyhow since it is vital to the understanding of how SchemeTalk works. As some reading material on the subject, you can have a look at the paper [Environments as First-Class Objects] and [LOL].
The main idea is very simple but powerful: an object is a procedure which takes a symbol and some extra values as input. The result is the result of evaluating a produre identified by the symbol.
We can start here with a very simple but stupid example of an “object”:
> (define i 0) > (define counter (lambda (msg . arguments) (set! i (+ i (car arguments))) i) > (counter 'inc 10) 10
Now we have global state and a function which increments the state every time you call it. Or in other words, each message you send to the object will result in an increment of a global variable i.
Obviously you don’t want objects to mutate global state, but rather it’s own state. The accessible state of an object here is bound by the lexical scope of a function. Knowing this, we can make the state of objects object-local by making sure that it’s only in the scope of the “function” which we use as object later on. We can do this as follows:
> (define counter (let ((i 0)) (lambda (msg . arguments) (set! i (car arguments)) i))) > (counter 'inc 10) 10
Now our counter doesn’t mutate global scope anymore, but only the i of the let in which the lambda is scoped. Since only the lambda sees the i, we have effectively created an object with only one slot, i. Now obviously we can start using the fact that we have more information inside the lambda. We could use the msg to invoke different kinds of behavior, and use the given arguments accordingly:
> (define counter (let ((i 0)) (lambda (msg . arguments) (case msg ((inc) (set! i (+ i (car arguments)))) ((print) (display i)) (else (error "Msg not understood: " msg arguments)))))) > (counter 'inc 10) > (counter 'print) 10
Counter in this case is a single object encapsulating it’s own behaviour. While useful, it’s often more useful to have a way of making new objects of the same kind. This can easily be done by just wrapping a function around the definition of the counter:
> (define make-counter (lambda () (let ((i 0)) (lambda (msg . arguments) (case msg ((inc) (set! i (+ i (car arguments)))) ((print) (display i)) (else (error "Msg not understood: " msg arguments))))))) > (define counter (make-counter)) > (counter 'inc 10) > (counter 'print) 10
If we execute “make-counter”, we generate a new scope and get a lambda back from that specific new scope. Each call to make-counter this generates a new object, as objects are defined by the scope in which the behaviour is enclosed.
To make it a bit more complete, we probably also want to initialize the “instance variables” to a specific value when we make a new object. This even simplified the resulting code:
> (define make-counter (lambda (i) (lambda (msg . arguments) (case msg ((inc) (set! i (+ i (car arguments)))) ((print) (display i)) (else (error "Msg not understood: " msg arguments)))))) > (define counter (make-counter 2)) > (counter 'print) 2 > (counter 'inc 4) > (counter 'print) 6
Since i is now an argument to the “class” make-counter, each time you create an “instance” i is initialized with the value passed to the “constructor”. And that’s that!
SchemeTalk builds further on this idea and builds an OO language (internal DSL?) in scheme with it, until the programmar can use the system as if he was programming in Smalltalk, rather than just using LOLs to emulate objects.