FPOO Chapters 2 & 3: Basic Objects in Elixir

[boilerplate bypath=”fp-oo”]

I feel like I should start with a disclaimer: this post is not advocating building an OO system on top of an FP language. And anyway, the Elixir/Erlang “process” model is arguably a very OO system right out of the box. But this series is about working through the FPOO book, and the exercise that’s up next is to implement a basic OO system on top of an FP language, so that’s what I’m going to do.

First version, without knowledge of class:

defmodule Objects1 do
  import Dict
  def new_point(x, y), do: [x: x, y: y]
  def x(point), do: get(point, :x)
  def y(point), do: get(point, :y)
end

(Note: when I wrote this I either didn’t know, or had forgotten, that subscript/square-bracket access was available in Elixir. So you’ll see a lot of get(point, :x) when I probably could have written point[:x].)

defmodule TestObjects1 do
  import FpOoElx.Exercises.Objects1
  test "constructing a Point" do
    p = new_point(3,5)
    assert(x(p) == 3)
    assert(y(p) == 5)
  end
end

Second version, with knowledge of class and shift method:

defmodule TestObjects2 do
  use ExUnit.Case
  import FpOoElx.Exercises.Objects2
  test "constructing a Point" do
    p = new_point(3,5)
    assert(x(p) == 3)
    assert(y(p) == 5)
    assert(class_of(p) == :point)
    p = shift(p, 7, -2)
    assert(x(p) == 10)
    assert(y(p) == 3)      
  end
  doctest FpOoElx.Exercises.Objects2
end
defmodule Objects2 do
  import Dict
  def new_point(x, y), do: [x: x, y: y, __class_symbol__: :point]
  def x(this), do: get(this, :x)
  def y(this), do: get(this, :y)   
  def class_of(object), do: get(object, :__class_symbol__)
  def shift(this, xinc, yinc), do: new_point(x(this) + xinc, y(this) + yinc)
  <<objects2>>
end

Exercise 1: Implement add

I think I’ll switch over to doctests instead of separate unit tests.

@doc """
## Examples:
    iex> p1 = new_point(3, 7)
    iex> p2 = new_point(8, -3)
    iex> p3 = add(p1, p2)
    iex> x(p3)
    11
    iex> y(p3)
    4
"""
def add(p1, p2), do: shift(p1, x(p2), y(p2))

Exercise 2: A “new” operator

If I did this exactly like the Clojure version I’d have to call it like this:

make(&new_point/1, [3, 5])

Blerg. I’ll make a macro instead.

@doc """
## Examples
    iex> p = make(point, [3, 5])
    iex> class_of(p)
    :point
    iex> x(p)
    3
    iex> y(p)
    5
"""
defmacro make(class, args) do
  {classname,_,_} = class
  constructor = binary_to_atom("new_#{classname}")
  quote do
    unquote(constructor)(unquote_splicing(args))
  end
end

OK, that was kinda cool. Of course, if I were willing to put up with passing the classname as a symbol rather than as a bareword, I wouldn’t need a macro.

@doc """
## Examples
    iex> p = make2(:point, [3, 5])
    iex> class_of(p)
    :point
    iex> x(p)
    3
    iex> y(p)
    5
"""
def make2(class, args) do
  constructor = :"new_#{class}"
  code = {constructor, [], args}
  {result, _} = Code.eval_quoted(code, binding, delegate_locals_to: __MODULE__)
  result
end

Note the use of delegate_locals_to: __MODULE__ to enable the eval_quoted to find methods in the current module. I’m still getting the hang of eval-ing in Elixir; there may be a better way to do this.

The next three exercises involve comparing triangles and I just can’t get excited about that, so I’m gonna stop here.

Leave a Reply

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