Charlie Harvey

Seven More Languages: Elixir Day Two

Today’s installment was long and occasionaly annoying. Mostly because I felt that what I wanted to say in Elixir was on the tip of my tongue. I just didn’t quite yet have the language to say it. An occupational hazard when picking up new languages.

We first looked at soome of the Elixir tooling that allows you to develop in a comfortable environment. Mix, the build-management tool is very straigntforward and we covered using it and developing tests in ExUnit. Then it was on to what is for me one of the most interesting features in Elixir, its macro system. The macros in Elixir are a little like Scheme or Lisp, but without the parens.

The chapter concentrates on building a state machine to represent a video store. Yes, it seemed anachronistic to me too. Especially in a book about languages that are shaping the future! It reminded me of South Park’s Nightmare on Facetime episode, probably one of my favourites.

Exercises

Easy exercises

Add a find state to the state machine that transitions from lost to found. Add this code in both the concrete and abstract versions of your state machine. Which is easier, and why?

I went a bit off-piste here because I think it makes sense for a video to become rented again once it has been found.

First the concrete implementation def find(v), do: fire(state_machine, v, :find) def state_machine do [ available: [ rent: [ to: :rented, calls: [&VideoStore.renting/1]] ], rented: [ return: [ to: :available, calls: [&VideoStore.returning/1] ], lose: [ to: :lost, calls: [&VideoStore.losing/1] ] ], lost: [ find: [ to: :rented, calls: [&VideoStore.finding/1]] ] ] end In the VideoStore module. def finding(v) do log v, "Finding #{v.title}" end

Now the abstract one state :lost, find: [ to: :rented, calls: [ &VidStore.finding/1 ] ] def finding(v) do log v, "Finding #{v.title}" end

Which is easier? The macro version. If I were to need to write a lot of functions it would certainly save me a bit of typing.

Medium exercises

Write tests for VidStore. What was different, and what was the same?

Here are some early tests that I wrote whilst I was working my way through the text. More to come. They are exactly the same as the tests for the concrete implementation.defmodule VidStoreTest do import Should use ExUnit.Case should "update count" do rv = VidStore.renting(video) assert rv.times_rented == 1 end should "rent video" do rv = VidStore.rent video assert :rented == rv.state assert 1 == Enum.count(rv.log) end should "handle multiple transitions" do import VidStore vid = video |> rent |> return |> rent |< return |< rent assert 5 == Enum.count(vid.log) assert 3 == vid.times_rented end def video, do: %Video{title: "XMen"} end

Hard exercises

Add before_(event_name) and after_(event_name) hooks. If those functions exist, make sure fire executes them.

This was really head-scratchy for me. I got there in the end after deleting my work and starting from scratch. It seems simple now I have done it. I imagine that there is a more idiomatic way to express the idea. I started off in the VidStore module, adding my hooks under the assumption that I would be able somehow to have them fire. def before_return(v) do log v, "Before returning #{v.title}" end def after_return(v) do log v, "After returning #{v.title}" end

I wrote a test and modified my existing tests to deal with the fact that there woulld be more records in the log once my hooks were firing. This is the complete final test code, with tests from the next exercise in it too.defmodule VidStoreTest do import Should use ExUnit.Case should "update count" do rv = VidStore.renting(video) assert rv.times_rented == 1 end should "rent video" do rv = VidStore.rent video assert :rented == rv.state assert 1 == Enum.count(rv.log) end should "renting, losing, finding and returning same as renting and returning" do import VidStore vid1 = video |> rent |> lose |> find |> return vid2 = video |> rent |> return # renting a video, then losing it and finding it then returning it # gives the same state as just rent and returning it assert vid1.state == vid2.state # admin checks assert 6 == Enum.count(vid1.log) assert 4 == Enum.count(vid2.log) assert 1 == vid1.times_rented assert 1 == vid2.times_rented end should "have record of hook activity after returning" do import VidStore vid = video |> rent |> return assert vid.log == ["After returning XMen\n", "Returning XMen\n", "Before returning XMen\n", "Renting XMen\n"] end should "handle multiple transitions" do import VidStore vid = video |> rent |> return |> rent |> return |> rent assert 9 == Enum.count(vid.log) assert 3 == vid.times_rented end should "choke on bad video (no state field)" do import VidStore assert_raise(KeyError, fn -> badvideo |> rent end) end def video, do: %Video{title: "XMen"} def badvideo, do: %BadVideo{title: "XMen"} end

Next, I implemented an hook function in the StateMachine module. It takes a module, a hook function name, a hook when describing when the hook is to fire(before_, after_) and a context. If there exists a function called when_function (with when being before or after), the function is applied to the context and a modified context returned. Otherwise the original context is passed back unmodified. def hook(mod,nm,whn,ctx) do func_name = whn <> to_string(nm) func = String.to_atom(func_name) if Kernel.function_exported?(mod, func, 1) do apply(mod, func, [ctx]) else ctx end end

Then I plumbed calls to hook into my already defined event_callback function. Note the altered contexts being passed along in the code. def event_callback(nm,mod) do callback = nm quote do def unquote(nm)(ctx) do ctx1 = hook(unquote(mod), unquote(nm), "before_", ctx) ctx2 = StateMachine.Behaviour.fire(state_machine, ctx1, unquote(callback)) ctx3 = hook(unquote(mod), unquote(nm), "after_", ctx2) end end end

Add a protocol to our state machine that forces a state machine struct to implement the state field.

This was a little simpler. Again, there may be a more idiomatic way to write this. I defined my proptocol in its own file, along with a default implementation that will throw a KeyError if it hits a missing key. defprotocol StateProtocol do @fallback_to_any true # allows us to provide a default implementation def statey?(data) end # Provide a default implementation # throws a KeyError if no state field in our struct defimpl StateProtocol, for: Any do def statey?(s), do: s.state end

Once that was in place, I could just add a single line to my StateMachine implementation so that all calls to ctx also call our StateProtocol. This might be expensive in real life. However, I try never to let performance get in the way of a quick answer though. def event_callback(nm,mod) do callback = nm quote do def unquote(nm)(ctx) do StateProtocol.statey?(ctx) ctx1 = hook(unquote(mod), unquote(nm), "before_", ctx) ctx2 = StateMachine.Behaviour.fire(state_machine, ctx1, unquote(callback)) ctx3 = hook(unquote(mod), unquote(nm), "after_", ctx2) end end end


Comments

  • Be respectful. You may want to read the comment guidelines before posting.
  • You can use Markdown syntax to format your comments. You can only use level 5 and 6 headings.
  • You can add class="your language" to code blocks to help highlight.js highlight them correctly.

Privacy note: This form will forward your IP address, user agent and referrer to the Akismet, StopForumSpam and Botscout spam filtering services. I don’t log these details. Those services will. I do log everything you type into the form. Full privacy statement.