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 behaviours and towards using protocols 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}),
        raise(ArgumentError, message: "tried to make a unquote(name) strategy, but got n")

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