FPOO Chapter 6: Inheritance

[boilerplate bypath=”fp-oo”]

Inheritance means knowing who your parent is.

def point() do 
  [
    __own_symbol__: :point,
    __superclass_symbol__: :anything,
    __instance_methods__: [
      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

Now to define the anything class, referenced above by the keyword :__superclass_symbol__.

def anything do
  [
    __own_symbol__: :anything,
    __instance_methods__: [
      add_instance_values: fn(this) -> this end,
      class_name: &get(&1, :__class_symbol__),
      class: fn 
        (this) -> apply(__MODULE__, get(this, :__class_symbol__), [])
      end
    ] 
  ]
end

And now the real magic: method lookup!

The top-level method lookup function:

def method_cache(class) do
  import Enum
  class_symbol = class |> get(:__own_symbol__)
  method_maps  = lineage(class_symbol) |> map(&class_instance_methods/1)
  method_maps |> reduce(&Dict.merge(&2, &1))
end

Notice that I’ve had to reverse the order of the arguments to Dict.merge/2. This is because reduce passes arguments in the order next_item, accumulator, but I want methods from each successive class in the lineage to override methods in earlier, less-specialized classes.

A helper to get the instance methods defined for a class:

def class_instance_methods(class_symbol) do
  apply(__MODULE__, class_symbol, []) |> get(:__instance_methods__)
end

Another helper to get the parent class symbol:

def class_symbol_above(class_symbol) do
  apply(__MODULE__, class_symbol, []) |> get(:__superclass_symbol__)
end

…and a function to get the ancestry chain of a given class:

@doc """
  iex> lineage(:point)
  [:anything, :point]
"""
def lineage(nil), do: []  
def lineage(class_symbol) do
  [class_symbol|class_symbol |> class_symbol_above |> lineage] |> Enum.reverse
end

Now to update apply_message_to to use this new code:

defp apply_message_to(class, object, message, args) do
  class |> method_cache |> get(message) |> apply([object|args])
end

Before I go any further, I feel a compulsion to try and golf down the method_cache function.

def method_cache(class) do
  import Enum
  class 
  |> get(:__own_symbol__)
  |> lineage
  |> map(&class_instance_methods/1)
  |> reduce(&Dict.merge(&2, &1))
end

Ah, lovely.

Exercise 1: factorial

Just for fun, I’ll do this one in a single definition instead of multiple pattern-matching definitions.

@doc """
  iex> factorial(5)
  120
"""
def factorial(n) do
  case n do
    0 -> 1
    1 -> 1
    _ -> n*factorial(n-1)
  end
end

Exercise 2: factorial, accumulator style

@doc """
  iex> factorial_acc(5)
  120
  iex> factorial_acc(0)
  1
"""
def factorial_acc(n, acc // 1) do
  if n == 0 || n == 1 do
    acc
  else
    factorial_acc(n-1, n*acc)
  end
end

Exercises 3-6: implementing reduce

I’m going to skip these for now.

Notes

One of the things I like about this section, and this book, is that Marick explicitly identifies functional patterns, such as the two styles of recursive method definition. He even shows “ideal forms” of the two patterns, with placeholders for the parts that change.

I’ve definitely seen these styles in use, but hadn’t seen them clearly elucidated as patterns before. If nothing else, this book should put the rest the notion, occasionally advanced, that patterns are somehow not applicable to functional programming. Every community develops patterns; being able to identify them and point out which parts are consistent and which parts change with the application is one of the marks of a good software writer.

Leave a Reply

Your email address will not be published. Required fields are marked *