Wizard Class – The Wizard Book – What are we doing here?



Wizard Class – The Wizard Book – What are we doing here?

0 0


wizard-class


On Github rskonnord-plos / wizard-class

Wizard Class

with Ryan Skonnord

The Wizard Book

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.

Why wizards?

“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...

Why wizards?

“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.

What are we doing here?

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)

Arcane Musings

Definition of an object

An object has

  • State
  • Behavior

Definition of object-oriented programming

Uses objects with the properties of:

  • Encapsulation
    • State that governs behavior is bundled up inside one object.
    • Includes, but does not require, information hiding.
  • Polymorphism
    • Multiple types of object that share behavior can be referred to with one common interface.
  • Inheritance
    • Subclasses can extend a class, keeping the parent's state and behavior while adding their own.

Definition of a class

  • A class defines the state and behavior that are common to a group of objects.
  • A class is an object!
  • The purpose of the class object is to construct other objects.
    • This is called instantiating the class.
    • The object made is called an instance of the class.

Semantics

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.

Encapsulation, Lisp-style

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))

Encapsulation, Lisp-style

  • Define a set of functions to create and act on an object.
  • In those functions, "privately" use any data structure you want to represent the object.
  • "Outsider" code uses those functions exclusively, and is agnostic to the data structure.
  • Ignoring the private implementation details is a matter of convention.
  • A failure in such discipline is called "violating the data abstraction".

Encapsulation, Lisp-style

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))

First-class functions

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)

First-class functions

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 );
        }
    });
});

Message passing

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.

Message passing in Python

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!

Closures

  • The rectangle functions were able to store information about their length and width.
  • Different instances of the function stored different values.
  • This is because they are not merely functions, but closures.
  • A closure is a function that holds on to a reference to the variables that existed when it was defined (its "environment").

Closures in languages

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.

Wizard words

More near-synonyms that are (ab)used interchangeably.

  • First-class function: A function that can be stored and passed around as a value.
  • Anonymous function: A first-class function declared within an expression, rather than in a definition block where it's given a name.
  • Lambda (λ): An operator for declaring an anonymous function.
  • Closure: A first-class function that's equipped with an environment (table of variables).
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])

Ground rules

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...

Ground rules

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.

Ground rules

I get to use primitive types and basic data structures (Array and Hash), including calling their methods.

  • If we really wanted to, we could implement these from scratch.
  • Message passing and closures are enough for the basic building blocks.
  • But, in the interests of time and sanity, we'll use Ruby's nice ones instead.

Ground rules

  • I am using Ruby to make basic classes, not making Ruby classes.
    • Ruby classes do awesome things.
    • awe·some /ˈôsəm/ adj. — extremely impressive or daunting; inspiring great admiration, apprehension, or fear.

Ground rules

And a few more caveats while I'm at it...

  • There will be mutations.
    • A purely functional solution is left as an exercise for Haskell enthusiasts.
  • There will be no information hiding.
    • Python doesn't do it.
    • Open access!

A few more disclaimers...

It will be a little more verbose than native classes.

Pretty syntax is a perk of having the language define classes for you.

My, these are a lot of disclaimers

Performance doesn't count.

  • We can't specify details like array storage unless the programming language lets us.
  • “Lisp programmers know the value of everything and the cost of nothing.” —Alan Perlis, Epigrams on Programming

Prices and participation may vary.Void where prohibited.

Also, no type-safety, error-handling, built-in methods, instance-of operator, overloading, or constructors.

Onto the main event!

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.

Wizard Class with Ryan Skonnord