On Github rskonnord-plos / wizard-class
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 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 4To 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!
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 athttp://rskonnord-plos.github.io/wizard-class/
Check out the code athttps://github.com/rskonnord-plos/wizard-class
These slides use reveal.js.
This work is licensed under a Creative Commons Attribution 4.0 International License.