SICP Classes for JavaScript
Warning: This article is intended for JavaScript programmers. Parens are coming but only briefly and you can handle it and it will be good for you.
In Structure and Interpretation of Programming Languages Second Edition (SICP) on page 182, the authors introduce the idea of message passing with the following example in Scheme of a complex number constructor function.
(define (make-from-real-imag x y)
(define (dispatch op)
(cond ((eq? op 'real-part) x)
((eq? op 'imag-part) y)
((eq? op 'magnitude)
(sqrt (+ (square x) (square y))))
((eq? op 'angle) (atan y x))
(else
(error "Uknown op -- MAKE-FROM-REAL-IMAG" op))))
dispatch)
The important part to note here is that the value returned by the make-from-real-imag
constructor function is actually a dispatch procedure that you can call with a message argument. You can send messages to get the real part or magnitude of the complex number.
(define c (make-from-real-imag 3 4))
(c 'real-part) ; 3
(c 'imag-part) ; 4
(c 'magnitude) ; 5
(c 'angle) ; 0.927295218001612
(c 'asdf) ; ERROR: Uknown op -- MAKE-FROM-REAL-IMAG: asdf
Let’s see what the above code looks like in JavaScript, our lingua franca.
function makeFromRealImag(x, y) {
function dispatch(op) {
switch (op) {
case 'realPart': return x;
case 'imagPart': return y;
case 'magnitude':
return Math.sqrt(x*x + y*y);
case 'angle': return Math.atan2(y, x);
default:
throw 'Unknown op -- makeFromRealImag: ' + op;
}
}
return dispatch;
}
var c = makeFromRealImag(3, 4);
c('realPart'); // 3
c('imagPart'); // 4
c('magnitude'); // 5
c('angle'); // 0.9272952180016122
c('asdf'); // "Unknown op -- makeFromRealImag: asdf"
Now this probably doesn’t look like any object-oriented JavaScript you’ve seen before but it illustrates an important point. In JavaScript, we can represent the idea of an object as a function of its messages. The constructor function returns a dispatch function that you wrote that can dispatch any message any way that you want it to. This immediately gives you Spidermonkey’s __nosuchmethod__
, Smalltalk’s doesNotUnderstand
, and Ruby’s method_missing
. Powerful stuff but unfortunately the JavaScript code above runs very slowly. We can move towards a faster and more familiar JavaScript style.
SICP page 223, introduces the idea of mutable objects but the most interesting point is the variation on the dispatch procedure.
(define (make-account balance)
(define (withdraw amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds"))
(define (deposit amount)
(set! balance (+ balance amount))
balance)
(define (dispatch m)
(cond ((eq? m 'withdraw) withdraw)
((eq? m 'deposit) deposit)
(else (error "Unknown request -- MAKE-ACCOUNT"
m))))
dispatch)
(define account (make-account 10))
((account 'deposit) 5) ; 15
((account 'withdraw) 3) ; 12
Converting this to JavaScript we have the following.
function makeAccount(balance) {
function withdraw(amount) {
if (balance >= amount) {
balance = balance - amount;
return balance;
}
return "Insufficient funds";
}
function deposit(amount) {
balance = balance + amount;
return balance;
}
function dispatch(m) {
switch (m) {
case 'withdraw': return withdraw;
case 'deposit': return deposit;
default:
throw "Unknown request -- makeAccount: " + m;
}
}
return dispatch;
}
var account = makeAccount(10);
account('deposit')(5); // 15
account('withdraw')(3); // 12
The way the dispatch
function works for accounts is quite different than in the complex numbers case. In the case of complex numbers, when a message was sent to the dispatch
function, it executed the associated operation (i.e. the method) immediately. In contrast, the account dispatch
function returns the method associated with the message and that method can then be called.
This is very similar to how JavaScript’s reciever.message(arg)
syntax works and we can move to a more familiar object-oriented JavaScript style.
function makeAccount(balance) {
function withdraw(amount) {
if (balance >= amount) {
balance = balance - amount;
return balance;
}
return "Insufficient funds";
}
function deposit(amount) {
balance = balance + amount;
return balance;
}
return {
withdraw: withdraw,
deposit: deposit
};
}
var account = makeAccount(10);
account.deposit(5); // 15
account.withdraw(3); // 12
In this last version, we’ve stopped writing our own dispatch logic and use JavaScript’s built-in property lookup. This increases speed significantly. We’ve lost the ability to do the __noSuchMethod__
type of dispatching when using standard ECMAScript but that doesn’t seem to be commonly useful anyway.
For the well-read JavaScript programmers out there, you may recognize this last version as durable objects from Douglas Crockford’s book JavaScript: The Good Parts.
I find it interesting that the word “inheritance” does not appear in SICP’s index even though the book goes on to implement complex programs like language interpreters and compilers in a message passing style. That shows this simple style of object-oriented programming can take you far.
The moral of the story is that old books are worth reading and can change the way you program today. You can even read SICP for free.
Comments
Have something to write? Comment on this article.
Encapsulation is important. This is especially true when working with multiple programmers so that monkey patching doesn’t start happening. Encapsulation also makes it clear what the necessary and intended scope of a variable is.
Nice article.
Maybe could be interesting to see the example of bank account using the other approach.
(define (make-account balance)
(define (withdraw amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds"))
(define (deposit amount)
(set! balance (+ balance amount))
balance)
(define (dispatch m a)
(cond ((eq? m 'withdraw) (withdraw a))
((eq? m 'deposit) (deposit a))
(else (error "Unknown request -- MAKE-ACCOUNT"))))
dispatch)
(define account (make-account 10))
(account 'deposit 5) ; 15
(account 'withdraw 3) ; 12
Now we can reproduce the behavior of the other version with the following functions:
(define (make-account-binder account)
(lambda (m)
(lambda (a) (account m a))))
(define binder (make-account-binder account))
(define depositer (binder 'deposit))
(define withdrawer (binder 'withdraw))
joseanpg,
That is a great additional version of make-account
. Thanks.
Peter, it’s rare be working with twenty objects each with fifty methods, do not you think?
In addition, for each object of that type, all the methods are sharing the same closured environment. Twenty lightweight structs for each object. It seems to me a small price to pay if we get encapsulation.
joseanpg,
It is not rare, at least for me, to be working with a set of objects with a total of 1000 methods. I have projects with hundreds of objects each with up to tens of methods.
This seems to be begging the question...
What are the performance/memory implications of using functional style objects?
Without benchmarks and memory consumption measurements, it’s really a meaningless discussion.
Javascript uses closures all over the place, so I wouldn’t assume that creating them is slow, or consumes a lot of memory. The code isn’t going to be reparsed, and rejitted.
Depending on how closures are implemented in a given runtime, there may be almost no overhead. All you need is a pointer back to the enclosed lexical environment.
Have something to write? Comment on this article.
So you’ve explained the factory (or closure) pattern? Or what am I missing?
I’m not sure why you’re such a huge fan of this pattern over the prototype way of doing this (at least, looking at your posts on es-discuss, twitter and now this blog post). The factory pattern is (imo) only really the way to go for security sensitive apps (like banks), due to the absence of "private" fields.
But while using it you’re creating new functions every time you call your .new method to create a new object. So when a certain object has twenty methods, and you create fifty of these objects, you’ve at the same time also created a thousand(+) function objects. Ouch. With prototype, this number would be fifty(+).
The lookup cost are about equal (closure lookup vs prototype chain lookup), depending on engine.
So could you explain why you seem to be pushing the factory pattern? I’m really just interested because I’m positive there’s a reason :)
- peter