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