Testing Individual Elixir Doctests
Doctests: FTW!
I’ve been working on new releases of a couple of my published Hex packages recently, as well as some new packages that should be soon published fairly soon.
All of them have doctests, sometimes a lot of them.
I think doctests are great; I’m a fan. “But there’s a problem”
Metaprogramming Without Macros
TL;DR: Code is Data!
Backstory
Originally this post was part of another post. But the whole post was getting too big, so I’ve pulled it out here.
Although really just part one, its really stand-alone and may be of more general interest.
The repo has the examples.
Metaprogramming with Macros
I guess anybody who knows anything, has read anything or used Elixir at all will be aware the core features of the language are implemented as a collection of macros – the special forms.
Elixir’s macro support is incredibly useful and I’d recommend anybody interested in learning some of the nitty-gritty to read Chris’s excellent book. Alternatively, there are also many excellent blog articles out there (like Sasa’s), and I’ve even had a go myself.
At the heart of macros are quoted forms, Elixir’s name for its abstract syntax tree (AST) representation.
When you run a macro, Elixir passes the arguments as quoted form(s) and expects quoted form(s) as the result (which will then be compiled). Inside the macro though, its just regular Elixir code, albeit manipulating ASTs.
I tend to think of the relationship (contract) between the Elixir compiler and a macro as simply this:
I’ll maybe pass you some asts, do what you want with them but give me back some asts and I’ll compile them for you.
But macros are not the only or best answer to some metaprogramming needs.
Sharing and Reusing Elixir Callback Functions between Modules
TL;DR: Sharing and Reusing Functions in Elixir is easy. Unless its a Callback.
There’s a problem?
Using a public Elixir function defined in one module (e.g. DonorA) in another (e.g. Recipient1) is easy: just call it with the fully qualified name:
1
2
3
4
5
6
7
8
9
10
defmodule DonorA do
def donor_a_fun_j do
:donor_a_fun_j
end
end
defmodule Recipient1 do
def recipient_a_fun_p do
DonorA.donor_a_fun_j
end
end
Proof of the pudding:
1
2
3
test "test_donor_a_recipient_1_donor_a_fun_j" do
assert :donor_a_fun_j = Recipient1.recipient_a_fun_p
end
Fine. Good. Job Done. End of Post. Thanks for reading.
No wait …
Pattern Matching to Polymorphism - an Unexpected Journey
When I first started learning Elixir, I didn’t begin to appreciate the power of pattern matching until I’d read Sasa’s book and Dave’s book. Chapter 5 in Dave’s book had some really good prose explaining pattern matching the arguments of anonymous and (Chapter 6) named functions.
But it took me a while to assimilate what the Elixir (and Erlang) books were telling me, so I started writing my Elixir functions rather ignorant of how I could better structure them.
But as ignorance was displaced by experience with Elixir, I started to use pattern matching for functions all over the place, once I’d “seen” that many of my functions were variations (variants) on the same theme, really families of functions with the same purpose.
After a while of using bread’n’butter function matching, I found I’d developed a style of using pattern matching to create a DSL with its own set of verbs.
Then I’d write dsl programs using the verbs. Coffee anyone?
Adding a Map API to a GenServer or Module with Agent-held State
TL;DR: A macro to generate a Map API for a GenServer’s state or a Module with Agent-held state
The Trouble with State
Modules often need to keep state and share it but, as getting started says
Elixir is an immutable language where nothing is shared by default.
In Elixir there are two basic ways of sharing state: processes and ETS (which I wont consider further).
Arguably the most common way of implementing process-held state is GenServer. But for a module that does little more than hold state, GenServer can be overkill. As an alternative, an Agent provides a simpler, alternative way for a module to hold and manage its state.
But neither GenServer nor Agent provide any built-in API calls to manage the content of the state, not surprisingly as the state’s value can be anything.
More often than not though, I want the state to be a map, and sometimes with keys that are (sub)maps. So I usually want to make Map-like API calls — get, put, take, update etc — both for the state itself and any submaps.
Previously, I’ve written the wrappers manually. This is not hard but is just a bit tedious to do given the possible combinations of state wrappers, submap wrappers, arities, etc (lots and lots of cut’n’paste’edit!). So recently I’ve bitten the bullet and written a first cut of a macro to do the heavy lifting.
The interface I wanted for my applications’ modules was to simply use the module (that I’ve called Amlapio) that makes the API wrapper functions.
As many will already know use calls the __using__ macro in the module being used, and the code generated by __using__ is injected into the caller module.
Here’s an example (from the repo’s tests) of a module using an Agent to hold its state.
The wanted submap wrappers are defined by the agent: [:buttons, :menus, :checkboxes] option.
To generate state wrappers the submaps have been set to nil: agent: nil
Generating wrappers for only a subset of Map functions can be given using the funs options e.g, for the state wrappers, funs: [:get, :put, :pop]
Finally, a namer function has been given to name the state wrappers. It is passed two arguments: the map name (e.g. buttons for a submap, but nil for a state wrapper) and the Map function name (e.g. pop).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
defmodule ExampleAgent1 do
# generate wrappers for three submaps
use Amlapio, agent: [:buttons, :menus, :checkboxes]
# generate *only* get, put and pop wrappers for the state itself and
# use a namer function to name the wrappers "agent_state_get",
# "agent_state_put" and "agent_state_pop"
use Amlapio, agent: nil, funs: [:get, :put, :pop],
namer: fn _map_name, fun_name ->
["agent_state_", to_string(fun_name)] |> Enum.join |> String.to_atom
end
# create the agent; note the default state is an empty map
def start_link(state \\ %{}) do
Agent.start_link(fn -> state end)
end
end
These tests from the repo show how the submap wrappers would be used, pretty much how you’d expect. (The repo has tests for the state wrappers as well.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
test "agent_submap1" do
buttons_state = %{1 => :button_back, 2 => :button_next, 3 => :button_exit}
menus_state = %{menu_a: 1, menu_b: :two, menu_c: "tre"}
checkboxes_state = %{checkbox_yesno: [:yes, :no], checkbox_bool: [true, false]}
agent_state = %{buttons: buttons_state,
menus: menus_state, checkboxes: checkboxes_state}
# create the agent
{:ok, agent} = ExampleAgent1.start_link(agent_state)
# some usage examples
assert :button_back == agent |> ExampleAgent1.buttons_get(1)
assert :button_default ==
agent |> ExampleAgent1.buttons_get(99, :button_default)
assert agent == agent |> ExampleAgent1.menus_put(:menu_d, 42)
assert menus_state |> Map.put(:menu_d, 42) == agent |> ExampleAgent1.agent_state_get(:menus)
assert {[:yes, :no], agent} ==
agent |> ExampleAgent1.checkboxes_pop(:checkbox_yesno)
end
Creating wrappers for a GenServer’s state is very similar. However, each wrapper has two “parts”: an api function and a handle_call function.
The api wrapper for e.g. `buttons_get/3` looks like this:
1
2
3
4
# api wrapper for buttons_get
def buttons_get(pid, button_name, button_default \\ nil) do
GenServer.call(pid, {:buttons_get, button_name, button_default})
end
… while the matching handle_call looks like:
1
2
3
4
def handle_call({:buttons_get, button_name, button_default}, _fromref, state) do
value = state |> Map.get(:buttons, %{}) |> Map.get(button_name, button_default)
{:reply, value, state}
end
Here’s example of generating wrappers for a GenServer.
Remember: all handle_call functions must be kept together in the source else the compiler will complain.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
defmodule ExampleGenServer1 do
# its a genserver
use GenServer
# generate API wrappers for three submaps
use Amlapio, genserver_api: [:buttons, :menus, :checkboxes]
# generate *only* get, put, pop and take wrappers for the state itself and
# use a namer function to name the wrappers "state_get",
# "state_put", "state_pop", and "state_take"
use Amlapio, genserver_api: nil, funs: [:get, :put, :pop, :take],
namer: fn _map_name, fun_name ->
["state_", to_string(fun_name)] |> Enum.join |> String.to_atom
end
# create the genserver; note the default state is an empty map
def start_link(state \\ %{}) do
GenServer.start_link(__MODULE__, state)
end
# << more functions>>
# handle_calls start here
# generate the handle_call functions for three submaps' wrappers
use Amlapio, genserver_handle_call: [:buttons, :menus, :checkboxes]
# generate the handle_call functions for the state wrappers.
use Amlapio, genserver_handle_call: nil, funs: [:get, :put, :pop, :take],
namer: fn _map_name, fun_name ->
["state_", to_string(fun_name)] |> Enum.join |> String.to_atom
end
end
There are tests got the GenServer example in the repo but they look almost identical to the Agent example.
If you just want to give it a whirl, that pretty much it. Its available on Hex and just needs to be added to the dependencies in mix.exs in the usual way.
1
{:amlapio, "~> 0.1.0"}
But if you are interested in a high-level explanation of how I implemented Amlapio, read on.