Search code examples
macroselixirunpackspecial-form

Elixir macros, quoting bitstring patternmatch types


I am working on a project in which I am likely to be writing a lot of code of the form:

defmodule Kind
  defstruct [name1, name2, name3]
  @type t :: %Kind{name1: integer(), name2: integer(), name3: binary()}
  def unpack(input) do
    with <<name1::integer-8>> <- Enum.take(input, 1),
      <<name2::integer-little-32>> <- Enum.take(input, 4),
      <<name3::binary-10>> <- Enum.take(input, 10),
    do: %Kind{name1: name1, name2: name2, name3: name3>>
  end
end

(for arbitrary sets of input names and types, input being a binary stream producing one byte at a time)

It would be very useful to be able to handle this in a macro, so that I could simply write (for example) use Unpack quote([{name1, integer-8}, {name2, integer-little-32}, {name3, binary-10}]) and automatically generate the necessary struct, typedef, and unpacking function, for arbitrary named fields of fixed sizes. Could even expand it, add a third field in the tuples to pass a function to handle variably-sized types. Unfortunately, when I try to do a simpler version of that (only taking one sized field, and only matching 1 to it):

defmodule Unpack do
  defmacro testmacro({name, kind}) do
    quote do
      <<unquote(name)::unqote(kind)>> = 1
    end
  end
end

The system tells me it's got invalid arguments for quote/1. I assume this is because the "types" used in bitstring pattern-matching are a special form, as are bitstring literals in general, and those particular items are not used anywhere else.

So, how do I get around that? I've got more than a dozen kinds of packed struct to unpack, each with between five and twenty different fields. If I don't do this I'll probably resort to Vim macros to at least save my hands... but that won't really help with having large amounts of extremely repetitive code to maintain.


Solution

  • Two things: you have a typo in unquote and the RHS must be a binary so that the pattern matches. With those changes, your code works for me:

    defmodule Unpack do
      defmacro unpack({name, kind}) do
        quote do
          <<unquote(name)::unquote(kind)>> = "a"
        end
      end
    end
    
    defmodule Main do
      import Unpack
    
      def main do
        unpack({foo, integer-8})
        IO.inspect foo
      end
    end
    
    Main.main
    

    Output:

    97