Elixir structs are are bare maps underneath. While they provide the nifty way to restrict keys to the predefined set, specify default values, require keys, pattern match selectively, and more, they are still bare maps with some syntactic sugar on top of them. Structs exports __struct__/1
method, allow protocol implementation inheritance and allow pattern match struct types to distinguish different structs by their definition in different clauses.
Structs purposedly do not allow to iterate them over out of the box. Neither do they provide the default Access
implementation. I would guess that’s because Elixir is very strict language in the sense of everything should work as expected no matter what, without exceptions. And neither Enumerable
nor Collectable
could not be ultimately defined for structs. Also, Access
behavior requires pop/3
to be implemented, which is impossible for structs be design.
But sometimes, you know, we are not as captious. We just want to make it working:
Enum.each(%MyStruct{}, ...)
# or
[foo: 42, bar: :baz] |> Enum.into(%MyStruct{})
# or
put_in(%MyStruct{}, [:foo], 42)
Once we understand all the consequences, we indeed have an ability to implement all the above for structs ourselves. It rather quickly becomes boring. In 99% of cases implementations are literally equal. Keeping in mind, that I nevertheless have plans to support structs for deep iterations in Iteraptor
, I decided to provide a syntactic sugar for implementing the above in custom structs. Please note, it probably won’t work in iex
for playing around, since protocol implementations should be consolidated, and they already are during iex
startup. Also, this won’t work for dynamically created modules. Access
, being a behaviour, would work though.
Syntax
To make the struct Enumerable
, implement an Access
and derive the implementation of the protocol Foo
, one should do inside the struct module:
use Iteraptor.Iteraptable skip: Collectable, derive: Foo
Arguments to keyword parameters might be both atoms or lists of atoms. Below I am going to share the approach I have taken. Another tiny tutorial on using macros in Elixir.
To enable Access
for the struct created dynamically, one might use
use Iteraptor.Iteraptable skip: [Enumerable, Collectable]
Enumerable
This would be the easiest one. Honestly, I am unsure why it’s not included by default. The implementation is correct by any mean and works for literally all structs without limitations. The only trick we need to exclude __struct__
key that contains the metainformation and is put into maps by Elixir itself to help both compiler and runtime to distinguish structs. So, delegate everything to the map
save for count/0
:
def count(map) do
# do not count :__struct__
{:ok, map |> Map.from_struct() |> map_size()}
end
I do not use {:ok, map_size(map) - 1}
since I want it to continue work properly when another private meta field will be added to structs by core team.
Collectable
It’s even shorter. I spent half of an hour thinking about how should I deal with an attempt to collect the key-value pair with a key not belonging to this struct, and found the simplest solution: I do not do anything. Struct itself will raise a proper error. Fail Fast.
defimpl Collectable, for: __MODULE__ do
def into(original) do
{original,
fn
map, {:cont, {k, v}} -> :maps.put(k, v, map)
map, :done -> map
_, :halt -> :ok
end}
end
end
Access
That was the hardest one. pop/3
contract is to pop up the value for the key and return a tuple {value, rest}
. The thing is I cannot remove keys from maps. I decided to nullify the value in the returned struct. Why not?
Besides the above, everything is quite straightforward.
use Iteraptor.Iteraptable
Here is the most exciting part. We need to embed the implementations into the caller’s context. Since we provide skip
parameter, we cannot just build an AST and inject it. The goal is to build AST selectively.
Also we want to raise on attempt to use
our helper in non-structs. Simple check for whether struct is declared on module won’t work, since defstruct
will be likely called after our code injection. But hey, it’s Elixir. We have compile hooks!
checker = quote(location: :keep, do: @after_compile({Iteraptor.Utils, :struct_checker}))
While we could simply inject __after_compile__/2
, that might conflict with the callback declared by the module owner. That’s why we delegate to our own function (that in turn simply calls env.module.__struct__
and allows Elixir to provide nifty error message is it is undefined.)
Then we prepare an AST for @derive
if needed.
And the most exciting part would be the selective injection of implementations. For that we prepare a map of the following structure:
@codepieces %{
Enumerable =>
quote location: :keep do
defimpl Enumerable, for: __MODULE__ do
...
Collectable => ...
and iterating through all the possible implementations, we check whether this one should not be skipped, and if not, we inject it:
Enum.reduce(@codepieces, [checker | derive], fn {type, ast}, acc ->
if Enum.find(excluded, &(&1 == type)), do: acc, else: [ast | acc]
end)
Voilà.
Usage
defmodule Iterapted do
use Iteraptor.Iteraptable
defstruct foo: 42, bar: :baz
end
Enum.each(%Iterapted{}, &IO.inspect/1)
#⇒ {:bar, :baz}
# {:foo, 42}
Happy iterating!