In 1985, some MIT professors wrote Structure and Interpretation of Computer Programs, known informally as "SICP" or "the Wizard Book".
This textbook was used for the introductory computer science course at MIT and UC Berkeley for decades.
An e-book edition is available for free and licensed under Creative Commons BY-SA.
My lecturer remarked (I'm paraphrasing) that, to this day, it covers the entire discipline of computer science, aside from implementation details.
It uses basic, but very elegant and powerful, concepts to explain a wide variety of topics in programming.
These concepts are expressed in the language Scheme, a dialect of Lisp.
“A computational process is indeed much like a sorcerer's idea of a spirit. It cannot be seen or touched. It is not composed of matter at all. However, it is very real. It can perform intellectual work. It can answer questions. It can affect the world...
“The programs we use to conjure processes are like a sorcerer's spells. They are carefully composed from symbolic expressions in arcane and esoteric programming languages that prescribe the tasks we want our processes to perform.”
—SICP, Chapter 1
A central theme of the book: code is data.
“It is no exaggeration to regard this as the most fundamental idea in programming: The evaluator, which determines the meaning of expressions in a programming language, is just another program.”
—SICP, Chapter 4
To illustrate this idea, the book spends a chapter implementing a Scheme interpreter in Scheme.
Let's do something similar: build an abstract programming tool out of more basic, but elegant, abstractions.
I was inspired by something that the faculty at Berkeley had added on to the course material: a library of macros that extended Scheme with object-oriented features.
Let's make our own classes.
I was also inspired by Matt Bowen's talk on abstraction layers.
Whereas Matt delved through multiple layers, I want to take a microscope to a single abstraction.
You might think of classes as magic: something that the programming language has to provide to us as a feature.
Well...
You're right. Classes are magic.
...and WE are the wizards!
class Person
attr_accessor :name
attr_accessor :weapon
def introduce
puts "Hello, my name is #{@name}."
end
def drink_tea
puts 'Slurp'
end
def fight(target)
unless @weapon.nil?
puts "#{@name} fights #{target.name}. #{@weapon.attack}"
end
end
end
class Wizard < Person
attr_accessor :hat_color
attr_accessor :is_humble
def introduce
if is_humble then
super
else
puts "Lo, behold, I am #{name} the #{hat_color}."
end
end
end
sam = Person.new
sam.name = 'Sam'
sam.introduce
gandalf = Wizard.new
gandalf.name = 'Gandalf'
gandalf.hat_color = 'Grey'
gandalf.introduce
saruman = Wizard.new
saruman.name = 'Saruman'
saruman.hat_color = 'White'
saruman.introduce
gandalf.is_humble = true
gandalf.introduce
class Staff
def attack
'Zap!'
end
end
class FireStaff < Staff
def attack
'Fwoosh! Kaboom!'
end
end
class FrostStaff < Staff
def attack
'Brr! Crack!'
end
end
gandalf.weapon = FireStaff.new
saruman.weapon = FrostStaff.new
gandalf.fight(saruman)
saruman.fight(gandalf)
An object has
Uses objects with the properties of:
Many languages don't model classes as objects.
Many programmers use "object" and "instance" interchangeably.
For that matter, many programmers use "instantiate" and "construct" interchangeably.
However, classes-as-objects isn't just an academic concept.
This is very relevant if you use any of the metaprogramming features in Python or in Ruby.
Before OOP became a buzzword, the Wizard Book used the term "data abstraction" to describe almost the same concept as encapsulation.
def make_rectangle(length, width):
return {'length': length, 'width': width}
def get_perimeter(rectangle):
return 2 * (rectangle['length'] + rectangle['width'])
def get_area(rectangle):
return rectangle['length'] * rectangle['width']
my_rectangle = make_rectangle(3, 5)
print('Perimeter is:', get_perimeter(my_rectangle))
print('Area is:', get_area(my_rectangle))
If you do it right, you can swap out the functions' private behavior and everything that uses them will work the same.
def make_rectangle(length, width):
return {'olinguito': length, 'keeshond': width}
def get_perimeter(rectangle):
return 2 * (rectangle['olinguito'] + rectangle['keeshond'])
def get_area(rectangle):
return rectangle['olinguito'] * rectangle['keeshond']
my_rectangle = make_rectangle(3, 5)
print('Perimeter is:', get_perimeter(my_rectangle))
print('Area is:', get_area(my_rectangle))
def square(x):
return x * x
def apply_twice_and_print(function, argument):
result = function(argument)
result = function(result)
print(result)
apply_twice_and_print(square, 4)
A more realistic example: callbacks.
$("button").click(function() {
$.ajax({
url: "http://example.com/my-service",
success: function(result, status, xhr) {
var formatted = formatResult(result);
$("#display").html(formatted);
},
error: function(xhr, status, error) {
$("#display").html(sadFace);
alert(error );
}
});
});
Data abstractions + First-class functions = Power
The message passing pattern gives us a way to bundle up both state and multiple kinds of behavior in a single first-class value.
def make_rectangle(length, width):
def receive_message(message):
if message == 'get_perimeter':
return 2 * (length + width)
if message == 'get_area':
return length * width
else: raise
return receive_message
def get_perimeter(rectangle):
return rectangle('get_perimeter')
def get_area(rectangle):
return rectangle('get_area')
small_rectangle = make_rectangle(3, 5)
big_rectangle = make_rectangle(30, 50)
print(get_area(small_rectangle), get_area(big_rectangle))
Note that the calculation logic has moved from the "get" functions to the object itself.
Message passing isn't the kind of design pattern you use in everyday coding, but it's really what happens under the covers of your favorite OOP language.
In fact, you don't generally write your own message-passing functions because OOP represents it so elegantly that you don't think about it.
No one go downstairs and check in code that looks like this.
public void processArticle(String message, Article article) {
if ("ingest".equals(message)) {
// ...
} else if ("render".equals(message)) {
// ...
} else if ("delete".equals(message)) {
// ...
}
}
It will not pass code review.
Message passing gives us encapsulation: data is bundled with behavior.
Message passing gives us polymorphism: you can create different types of object that recognize common messages.
We're two thirds of the way there!
Language | First-class functions? | Lambdas? | Closures? |
---|---|---|---|
Python, Ruby, JavaScript | Yes | Yes | Yes |
C | Sort of | No | No |
Java | Fakes it pretty well | Yes, in Java 8! |
Fakes it awkwardly |
In Java, what you see is this:
public long getCount(final String name) {
return hibernateTemplate.execute(new HibernateCallback<Long>() {
@Override public Long doInHibernate(Session session) throws HibernateException, SQLException {
Query query = session.createQuery("select count(*) from Thing where name=:name");
query.setParameter("name", name);
return (Long) query.uniqueResult();
}
});
}
What it's actually doing is this:
public long getCount(final String name) {
return hibernateTemplate.execute(new InvisibleAnonymousClass(name));
}
private class InvisibleAnonymousClass implements HibernateCallback<Long> {
private final String name;
private InvisibleAnonymousClass(String name) {
this.name = name;
}
@Override public Long doInHibernate(Session session) throws HibernateException, SQLException {
Query query = session.createQuery("select count(*) from Thing where name=:name");
query.setParameter("name", name);
return (Long) query.uniqueResult();
}
}
Java forces you to declare name
as final
because you can't see
when it is passed to
the invisible constructor.
More near-synonyms that are (ab)used interchangeably.
def square(x):
return x * x
map(square, # First-class, but not anonymous
[1, 2, 3])
map((lambda x: x * x), # Anonymous
[1, 2, 3])
Obviously it would defeat the purpose to use Ruby's object-oriented features to implement object-oriented programming.
But there will be a few exceptions...
I get to call the call
method on a first-class function.
Ruby's syntax doesn't otherwise make it possible to distinguish between referring to a lambda-defined function and calling it.
I get to use primitive types and basic data structures (Array
and Hash
), including
calling their methods.
And a few more caveats while I'm at it...
It will be a little more verbose than native classes.
Pretty syntax is a perk of having the language define classes for you.
Performance doesn't count.
Also, no type-safety, error-handling, built-in methods, instance-of operator, overloading, or constructors.
def make_object(message_table)
return (lambda do |message, *message_args|
message_table[message].call(*message_args)
end)
end
def ask(object, message, *message_args)
return object.call(message, *message_args)
end
def make_object(message_table)
return (lambda do |message, *message_args|
message_table[message].call(*message_args)
end)
end
def ask(object, message, *message_args)
return object.call(message, *message_args)
end
def make_class()
class_message_table = {}
new_class = make_object(class_message_table)
class_message_table[:instantiate] = lambda do | |
instance_message_table = {}
new_instance = make_object(instance_message_table)
return new_instance
end
return new_class
end
Person = make_class()
sam = ask(Person, :instantiate)
puts sam
def make_object(message_table)
return (lambda do |message, *message_args|
message_table[message].call(*message_args)
end)
end
def ask(object, message, *message_args)
return object.call(message, *message_args)
end
def make_class()
class_message_table = {}
new_class = make_object(class_message_table)
class_message_table[:instantiate] = lambda do | |
fields = {}
instance_message_table = {}
new_instance = make_object(instance_message_table)
instance_message_table[:get_field] = lambda do |field_name|
return fields[field_name]
end
instance_message_table[:set_field] = lambda do |field_name, value|
fields[field_name] = value
end
return new_instance
end
return new_class
end
Person = make_class()
sam = ask(Person, :instantiate)
ask(sam, :set_field, :name, 'Sam')
puts ask(sam, :get_field, :name)
def make_object(message_table)
return (lambda do |message, *message_args|
message_table[message].call(*message_args)
end)
end
def ask(object, message, *message_args)
return object.call(message, *message_args)
end
def make_class(method_table)
class_message_table = {}
new_class = make_object(class_message_table)
class_message_table[:instantiate] = lambda do | |
fields = {}
instance_message_table = {}
new_instance = make_object(instance_message_table)
instance_message_table[:get_field] = lambda do |field_name|
return fields[field_name]
end
instance_message_table[:set_field] = lambda do |field_name, value|
fields[field_name] = value
end
instance_message_table[:call_method] = lambda do |method_name, *method_args|
method = method_table[method_name]
return method.call(new_instance, *method_args)
end
return new_instance
end
return new_class
end
Person = make_class({
:introduce => lambda do |this|
name = ask(this, :get_field, :name)
puts "Hello, my name is #{name}."
end,
})
sam = ask(Person, :instantiate)
ask(sam, :set_field, :name, 'Sam')
ask(sam, :call_method, :introduce)
def make_object(message_table)
return (lambda do |message, *message_args|
message_table[message].call(*message_args)
end)
end
def ask(object, message, *message_args)
return object.call(message, *message_args)
end
def make_class(parents, method_table)
class_message_table = {}
new_class = make_object(class_message_table)
class_message_table[:get_method] = lambda do |method_name|
method = nil
if method_table.has_key? method_name
method = method_table[method_name]
else
parents.each do |parent|
method = ask(parent, :get_method, method_name)
unless method.nil?
break
end
end
end
return method
end
class_message_table[:instantiate] = lambda do | |
fields = {}
instance_message_table = {}
new_instance = make_object(instance_message_table)
instance_message_table[:get_field] = lambda do |field_name|
return fields[field_name]
end
instance_message_table[:set_field] = lambda do |field_name, value|
fields[field_name] = value
end
instance_message_table[:call_method] = lambda do |method_name, *method_args|
method = ask(new_class, :get_method, method_name)
return method.call(new_instance, *method_args)
end
return new_instance
end
return new_class
end
Person = make_class([],
{
:introduce => lambda do |this|
name = ask(this, :get_field, :name)
puts "Hello, my name is #{name}."
end,
:drink_tea => lambda do |this|
puts 'Slurp'
end,
})
Wizard = make_class([Person],
{
:introduce => lambda do |this|
name = ask(this, :get_field, :name)
hat_color = ask(this, :get_field, :hat_color)
puts "Lo, behold, I am #{name} the #{hat_color}."
end
})
sam = ask(Person, :instantiate)
ask(sam, :set_field, :name, 'Sam')
ask(sam, :call_method, :introduce)
ask(sam, :call_method, :drink_tea)
gandalf = ask(Wizard, :instantiate)
ask(gandalf, :set_field, :name, 'Gandalf')
ask(gandalf, :set_field, :hat_color, 'Grey')
ask(gandalf, :call_method, :introduce)
ask(gandalf, :call_method, :drink_tea)
def make_object(message_table)
return (lambda do |message, *message_args|
message_table[message].call(*message_args)
end)
end
def ask(object, message, *message_args)
return object.call(message, *message_args)
end
def make_class(parents, method_table)
class_message_table = {}
new_class = make_object(class_message_table)
class_message_table[:get_method] = lambda do |method_name|
method = nil
if method_table.has_key? method_name
method = method_table[method_name]
else
parents.each do |parent|
method = ask(parent, :get_method, method_name)
unless method.nil?
break
end
end
end
return method
end
class_message_table[:get_usual_method] = lambda do |method_name|
return method_table[method_name]
end
class_message_table[:instantiate] = lambda do | |
fields = {}
instance_message_table = {}
new_instance = make_object(instance_message_table)
instance_message_table[:get_field] = lambda do |field_name|
return fields[field_name]
end
instance_message_table[:set_field] = lambda do |field_name, value|
fields[field_name] = value
end
instance_message_table[:call_method] = lambda do |method_name, *method_args|
method = ask(new_class, :get_method, method_name)
return method.call(new_instance, *method_args)
end
instance_message_table[:call_usual_method] = lambda do |class_obj, method_name, *method_args|
method = ask(class_obj, :get_usual_method, method_name)
return method.call(new_instance, *method_args)
end
return new_instance
end
return new_class
end
Person = make_class([],
{
:introduce => lambda do |this|
name = ask(this, :get_field, :name)
puts "Hello, my name is #{name}."
end,
:drink_tea => lambda do |this|
puts 'Slurp'
end,
})
Wizard = make_class([Person],
{
:introduce => lambda do |this|
if ask(this, :get_field, :is_humble) then
ask(this, :call_usual_method, Person, :introduce)
else
name = ask(this, :get_field, :name)
hat_color = ask(this, :get_field, :hat_color)
puts "Lo, behold, I am #{name} the #{hat_color}."
end
end
})
sam = ask(Person, :instantiate)
ask(sam, :set_field, :name, 'Sam')
ask(sam, :call_method, :introduce)
ask(sam, :call_method, :drink_tea)
gandalf = ask(Wizard, :instantiate)
ask(gandalf, :set_field, :name, 'Gandalf')
ask(gandalf, :set_field, :hat_color, 'Grey')
ask(gandalf, :call_method, :introduce)
ask(gandalf, :call_method, :drink_tea)
ask(gandalf, :set_field, :is_humble, true)
ask(gandalf, :call_method, :introduce)
def make_object(message_table)
return (lambda do |message, *message_args|
message_table[message].call(*message_args)
end)
end
def ask(object, message, *message_args)
return object.call(message, *message_args)
end
def make_class(parents, method_table)
class_message_table = {}
new_class = make_object(class_message_table)
class_message_table[:get_method] = lambda do |method_name|
method = nil
if method_table.has_key? method_name
method = method_table[method_name]
else
parents.each do |parent|
method = ask(parent, :get_method, method_name)
unless method.nil?
break
end
end
end
return method
end
class_message_table[:get_usual_method] = lambda do |method_name|
return method_table[method_name]
end
class_message_table[:instantiate] = lambda do | |
fields = {}
instance_message_table = {}
new_instance = make_object(instance_message_table)
instance_message_table[:get_field] = lambda do |field_name|
return fields[field_name]
end
instance_message_table[:set_field] = lambda do |field_name, value|
fields[field_name] = value
end
instance_message_table[:call_method] = lambda do |method_name, *method_args|
method = ask(new_class, :get_method, method_name)
return method.call(new_instance, *method_args)
end
instance_message_table[:call_usual_method] = lambda do |class_obj, method_name, *method_args|
method = ask(class_obj, :get_usual_method, method_name)
return method.call(new_instance, *method_args)
end
return new_instance
end
return new_class
end
Person = make_class([],
{
:introduce => lambda do |this|
name = ask(this, :get_field, :name)
puts "Hello, my name is #{name}."
end,
:drink_tea => lambda do |this|
puts 'Slurp'
end,
:fight => lambda do |this, target|
weapon = ask(this, :get_field, :weapon)
unless weapon.nil?
name = ask(this, :get_field, :name)
target_name = ask(target, :get_field, :name)
weapon_attack = ask(weapon, :call_method, :attack)
puts "#{name} fights #{target_name}. #{weapon_attack}"
end
end,
})
Wizard = make_class([Person],
{
:introduce => lambda do |this|
if ask(this, :get_field, :is_humble) then
ask(this, :call_usual_method, Person, :introduce)
else
name = ask(this, :get_field, :name)
hat_color = ask(this, :get_field, :hat_color)
puts "Lo, behold, I am #{name} the #{hat_color}."
end
end
})
sam = ask(Person, :instantiate)
ask(sam, :set_field, :name, 'Sam')
ask(sam, :call_method, :introduce)
ask(sam, :call_method, :drink_tea)
gandalf = ask(Wizard, :instantiate)
ask(gandalf, :set_field, :name, 'Gandalf')
ask(gandalf, :set_field, :hat_color, 'Grey')
ask(gandalf, :call_method, :introduce)
ask(gandalf, :call_method, :drink_tea)
saruman = ask(Wizard, :instantiate)
ask(saruman, :set_field, :name, 'Saruman')
ask(saruman, :set_field, :hat_color, 'White')
ask(saruman, :call_method, :introduce)
ask(gandalf, :set_field, :is_humble, true)
ask(gandalf, :call_method, :introduce)
Staff = make_class([],
{
:attack => lambda do |this|
'Zap!'
end
})
FireStaff = make_class([Staff],
{
:attack => lambda do |this|
'Fwoosh! Kaboom!'
end
})
FrostStaff = make_class([Staff],
{
:attack => lambda do |this|
'Brr! Crack!'
end
})
ask(gandalf, :set_field, :weapon, ask(FireStaff, :instantiate))
ask(saruman, :set_field, :weapon, ask(FrostStaff, :instantiate))
ask(gandalf, :call_method, :fight, saruman)
ask(saruman, :call_method, :fight, gandalf)
View these slides at
http://rskonnord-plos.github.io/wizard-class/
Check out the code at
https://github.com/rskonnord-plos/wizard-class
These slides use reveal.js.
This work is licensed under a Creative Commons Attribution 4.0 International License.