FPOO Chapter 5: Classes

[boilerplate bypath=”fp-oo”]

So far, my tiny object system in Elixir has been stowing all methods directly in instances. Chapter 5 of FPOO directs me to move instance methods out into a “class” of some kind.

First off, there’s no more new_point . In its place, a simple keyword list defining attributes of the class. The instance variables are no longer hard-coded in the keyword list. Instead, there is a new add_instance_values callback that plays the role of an initializer method.

def point() do 
  [
    __own_symbol__: :point,
    __instance_methods__: [
      class: &get(&1, :__class_symbol__),
      add_instance_values: fn (this, x, y) ->
                                this |> merge([x: x, y: y])
                           end,
      shift: fn (this, xinc, yinc) -> 
                  make(point, [get(this, :x) + xinc, get(this, :y) + yinc])
             end
    ]
  ]
end

Next up, a new make function that can use this style of class definition.

def make(class, args) do
  allocated   = []
  seeded      = allocated |> merge([__class_symbol__: get(class, :__own_symbol__)])
  constructor = class |> get(:__instance_methods__) |> get(:add_instance_values)
  apply(constructor, [seeded|args])
end

Now for message dispatch.

def send_to(object, message, args // []) do
  class_name = object |> get(:__class_symbol__) 
  class      = apply(__MODULE__, class_name, [])
  method     = class |> get(:__instance_methods__) |> get(message)
  apply(method, [object|args])
end

I gotta say, I kind of prefer Elixir pipelines for chaining keyword gets:

class |> get(:__instance_methods__) |> get(message)

…to Clojure nested function calls:

(let [method (message (:__instance_methods__ class))])

(EDIT: I had either forgotten or didn’t yet know about subscript ([]) access when this was written)

Before I go any further, let’s see if any of this is working.

test "class-based object creation" do
  import Dict
  p = make(point, [23, 42])
  assert(get(p, :x) == 23)
  assert(get(p, :y) == 42)
  p2 = send_to(p, :shift, [2, 3])
  assert(get(p2, :x) == 25)
  assert(get(p2, :y) == 45)
end

Exercise 1: apply-message-to

A small refactoring to pull out this helper method:

def apply_message_to(class, object, message, args) do
  method = class |> get(:__instance_methods__) |> get(message)
  apply(method, [object|args])
end

Now the make and send_to functions can be refactored to use this new helper method.

def apply_message_to(class, object, message, args) do
  method = class |> get(:__instance_methods__) |> get(message)
  apply(method, [object|args])
end
def send_to(object, message, args // []) do
  class_name = object |> get(:__class_symbol__) 
  class      = apply(__MODULE__, class_name, [])
  apply_message_to(class, object, message, args)
end

Exercise 2: class and class-name

The behavior I want:

test "class and class name" do
  p = make(point, [23, 42])
  assert(send_to(p, :class) == point)
  assert(send_to(p, :class_name) == :point)
end

This necessitates a change to the class definition:

def point() do 
  [
    __own_symbol__: :point,
    __instance_methods__: [
      class_name: &get(&1, :__class_symbol__),
      class: fn (_this) -> point end,
      add_instance_values: fn (this, x, y) ->
                                this |> merge([x: x, y: y])
                           end,
      shift: fn (this, xinc, yinc) -> 
                  make(point, [get(this, :x) + xinc, get(this, :y) + yinc])
             end
    ]
  ]
end