Object Oriented Programming with closures
I’ve been recently watching Structure and Interpretation of Computer Programs. These are great lectures that I highly recommend to everyone interested in programming. Some people can be discouraged by the use of Scheme, don’t be. Scheme is a tiny language, very easy to pickup and follow, and many techniques from the lectures are applicable to today’s mainstream languages.
One interesting idea is that closures are enough to implement an object-oriented system. A simple example is a counter object. In JavaScript it can be implemented like this:
function counter() {
var val = 0;
function get() {
return val;
}
function inc() {
val += 1;
}
return [get, inc];
}
function get_count(counter) {
return counter[0]();
}
function inc_count(counter) {
counter[1]();
}
var counter = counter();
inc_count(counter);
inc_count(counter);
console.log(get_count(counter)); // prints 2
A statefull counter exposes a public interface for increasing and obtaining a current count. The internals are encapsulated, you can not reset or decrease the count, because such operations are not exposed by the API.
You can easily define your own version of the counter, and use it in any place where the counter is expected:
function fast_counter() {
var val = 0;
function get() {
return val;
}
function inc() {
val += 2;
}
return [get, inc];
}
The implementation above makes one shortcut: a JavaScript built-in Array object is used to store and dispatch counter’s internal methods. This is convenient, but not necessary. The lectures show that closures are enough to implement data structures without a need for any built-in complex data types (‘out of thin air’ as lecturers call it). The trick is stunning and really worth studying. First we need a function that creates a pair:
function pair(first, second) { // In Lisp called `cons`
function set_first(value) {
first = value;
}
function set_second(value) {
second = value;
}
return function(handler) {
return handler(first, second, set_first, set_second);
};
}
A pair returned by the function is not data, but code: a closure. The closure needs to be invoked with a function as an argument, and it gives this function permissions to access and modify the values stored in the pair.
Here is how the first element of the pair can be extracted:
function first(pair) { // aka car
return pair(function(first, second, set_first, set_second) {
return first;
});
}
first() invokes a pair() and passes a function to it. The function is then invoked by the pair with its two current values and two functions to change the current values. first() cares only about the value of the first element and ignores remaining arguments.
Below are three more analogous functions to operate on the pair:
function second(pair) { // aka cdr
return pair(function(first, second, set_first, set_second) {
return second;
});
}
function set_first(pair, value) { // aka set-car!
return pair(function(first, second, set_first, set_second) {
set_first(value);
});
}
function set_second(pair, value) { // aka set-cdr!
return pair(function(first, second, set_first, set_second) {
set_second(value);
});
}
The internals are hairy, but resulting high level interface is simple:
var p = pair(3, 4);
console.log(first(p) + " : " + second(p));
set_first(p, 5);
console.log(first(p) + " : " + second(p));
And pairs are enough to build a list:
var l = pair(3, pair(4, pair(5, null)));
Of course, a higher level interface can be defined, this time very straight-forward:
function list() {
return null;
}
function is_empty(list) {
return list === null;
}
function last(list) {
if (is_empty(list) || second(list) === null) {
return list;
}
return last(second(list));
}
function length(list) {
if (is_empty(list)) {
return 0;
}
return 1 + length(second(list));
}
function append(list, value) {
if (is_empty(list)) {
return pair(value, null);
}
set_second(last(list), pair(value, null));
return list;
}
function get_item(list, idx) {
if (idx === 0) {
return first(list);
}
return get_item(second(list), idx - 1);
}
var l = list();
console.log(is_empty(l));
l = append(l, 1);
l = append(l, 2);
l = append(l, 3);
console.log(is_empty(l));
console.log(get_item(l, 1));
console.log(length(l));
This is just an exercise, even in Lisp pairs (cons cells) are unlikely to be implemented this way. But it is interesting to see how powerful closures are, and how they allow to blur the distinction between code and data.
wwwhisper add-on released
The authorization add-on for Heroku is finally out of beta. A huge thank you to everyone who has tried the add-on during the testing phases!
Plans and pricing
Three plans are currently available:
- Starter is a free plan that allows to grant access to a protected application to a single user. The plan gives an easy way to try wwwhisper. It should also cover the needs of people who are developing web applications alone or who would like to conveniently host any privately accessible application on Heroku.
- Basic plan allows to grant access to an application to up to ten people. The plan should cover the needs of a team working together, or sharing an application with clients or investors. It is also intended for small publicly accessible sites that need to have some locations protected, for example an admin web interface. The add-on allows to easily restrict access to such locations and frees developers from worrying about the authorization logic. It can also provide protection if an available authorization mechanism has security holes.
- Plus plan supports sites with bigger traffic and allows for up to 100 authorized users.
Roadmap
If you have any feedback about available features or pricing please send an email to wwwhisper-service@mixedbit.org. This will greatly help to prioritize a todo list. Currently following items are considered:
- Authorization middleware for more frameworks (node.js, wsgi, ...).
- An option to set custom texts on a sign-in page.
- Improvements to the admin web interface to conveniently manage 100+ users.
- A plugin for a heroku command line tool to perform wwwhisper admin operations (grant, revoke access).
- Allow to use the service outside of the Heroku platform.