A Fun Little Elixir Macro
Andrew Fontaine <andrew@afontaine.ca>
I’m working on a big refactoring of my one elixir library, unleash
,
and one of the changes I’m working on is moving from using behaviour
s
and towards using protocol
s instead, but this means parsing a bunch
of JSON objects into their appropriate structs. While Poison
can
already do this, I don’t want to be dependant on a single JSON library
(for now), and it doesn’t quite tackle the problem at hand, so I got
to work.
After migrating two of the eight strategies to this new format, I saw a lot of boilerplate, and got to work (learning to) writing a macro.
Oh no, Macro
The usage is simple:
defstrategy "name", keys: 0, in: [], parameters: ""
Here "name"
is the name of the strategy, and is utilized in parsing.
The rest is a keyword list that matches a struct definition. I need to
work in a bit of the ability to parse these parameters somehow, as
Unleash generally tends to send everything as a string, but that’s still
being puzzled out.
And the magic is:
defmacro defstrategy(name, parameters) do
# A little bit of early magic to parse the parameters into the AST for
# a map with string keys.
#
# [keys: 0, in: []] -> %{"keys" => arg0, "in" => arg1}
#
# This is used below in the function definition to make a function
# that pattern matches on the expected shape.
members =
Enum.map(parameters, fn {k, _} -> {Atom.to_string(k), Macro.var(k, __CALLER__.module)} end)
|> then(&{:%{}, [], &1})
# Similar to the above, creates the AST of a map with atom keys, as
# that is what is expected when creating a new struct
#
# [keys: 0, in: []] -> %{keys: arg0, in: arg1}
#
# This is used as the parsing function body to map to a new struct!
struct =
Enum.map(parameters, fn {k, _} -> {k, Macro.var(k, __CALLER__.module)} end)
|> then(&{:%{}, [], &1})
quote do
# Define a struct!
defstruct unquote(parameters)
# As explained above, expands to
# def from_map!(%{name: "name", parameters: %{"keys" => arg0, "in" => arg1}})
def from_map!(%{name: unquote(name), parameters: unquote(members)}),
# As explained above, expands to
# do: struct!(__MODULE__, %{keys: arg0, in: arg1})
do: struct!(__MODULE__, unquote(struct))
# If it doesn't match, raise an argument error, as we crash early in elixir
def from_map!(%{name: n}),
do:
raise(ArgumentError, message: "tried to make a unquote(name) strategy, but got n")
end
end
As I work on this, I might pull it out into its own library, as I think it might make sense, especially as I add more functionality (optional keys? parsing values further?). I also realize this has taken a lot of inspiration from Parse, don’t validate from Alexis King, and is an excellent read, even if elixir is not staticly typed. It couples nicely with the “let it crash” mentality of the BEAM ecosystem. If your feature flag configuration is broken, I probably don’t know what you’re trying to do.
Anyway, just a little fun that I’ve been working on.
Want to discuss this post?
Reach out via email to ~afontaine/blog-discuss@lists.sr.ht, and be sure to follow the mailing list etiquette.
Other posts