Generating pattern matching functions based on a module attribute

Trying to get this to work…

defmacro __before_compile__(_env) do
  quote do
    Enum.each(@decoders, fn {type, decoder} ->
      def decode(unquote(type), buffer) do
        unquote(decoder).decode(buffer)
      end
    end)
  end
end

I want it to generate functions like:

def decode("integer", buffer), do: IntegerDecoder.decode(buffer)
def decode("binary", buffer), do: BinaryDecoder.decode(buffer)

When the value of @decoders is:

[
  {"integer", IntegerDecoder},
  {"binary", BinaryDecoder}
]

Thanks for the help!

Not sure if that’s your exact scenario but this works:

defmodule IntegerDecoder do
  def decode(buffer) do
    IO.puts("#{__MODULE__}: #{inspect(buffer)}")
  end
end

defmodule BinaryDecoder do
  def decode(buffer) do
    IO.puts("#{__MODULE__}: #{inspect(buffer)}")
  end
end

defmodule Injector do
  @decoders [
    {"integer", IntegerDecoder},
    {"binary", BinaryDecoder}
  ]

  defmacro __before_compile__(_env) do
    quote bind_quoted: [decoders: @decoders] do
      for {type, decoder} <- decoders do
        def decode(unquote(type), buffer) do
          unquote(decoder).decode(buffer)
        end
      end
    end
  end
end

defmodule Recipient do
  @before_compile Injector
end

In iex:

iex(1)> Recipient.decode("binary", 'bb')
Elixir.BinaryDecoder: 'bb'
:ok
iex(2)> Recipient.decode("integer", 'aa')
Elixir.IntegerDecoder: 'aa'
:ok
1 Like
  defmacro __before_compile__(_env) do
    quote unquote: false do
      for {type, decoder} <- @decoders do
        def decode(unquote(type), buffer) do
          unquote(decoder).decode(buffer)
        end
      end
    end
  end

Should do it. for comprehension is a macro, hence unquote: false is required to establish the right macro scope.

4 Likes

I would write this without __before_compile__:

defmodule GenDecoder do
  @decoders [
    {"integer", IntegerDecoder},
    {"binary", BinaryDecoder}
  ]

  for {type, module} <- @decoders do
    def decode(unquote(type), buffer), do: unquote(module).decode(buffer)
  end
end

defmodule IntegerDecoder do
  def decode(buffer), do: "int:#{buffer}"
end

defmodule BinaryDecoder do
  def decode(buffer), do: "binary:#{buffer}"
end

Example of using the code:

iex(1)> GenDecoder.decode("integer", "a buffer")
"int:a buffer"
iex(2)> GenDecoder.decode("binary", "a buffer")
"binary:a buffer"
3 Likes

why even have a macro?

def decode(type, buffer), do: @decoders[type].decode(buffer)
3 Likes

Starting with that is generally a good idea. If this is indeed a hot path reducing the callstack can be helpful though.

Thanks for all the replies!

I would write this without __before_compile__

I thought about that too, but again struggled with implementation.

defmodule MyCoder do
  use GenCoder

  import_coder(IntegerDecoder)
  import_coder(BinaryDecoder)
end

defmodule GenCoder do
  defmacro import_decoder(module) do
    quote unquote: false do
      module = unquote(module)
      type = module.get_type()
      def decoder(unquote(type), buffer), do: unquote(module).decode(buffer)
    end
  end
end

(this doesn’t work :point_up_2: )

why even have a macro?

If this is indeed a hot path…

Yeah, I was under the impression that pattern matching in Elixir/Erlang is extremely fast; much faster than map lookup. And people tend to want these binary encoders/decoders to be pretty quick.

For posterity (and people’s opinion), I’m writing an extendable BSON library. The goal is to easily be able to add new types beyond the official spec (for example, add a type for Date).

Protocols work great for encoding, but for decoding, need to have a user configurable dispatch table.

Something like this…

defimpl Beson.Encoder, for: Date do
  @bson_type <<0x20>> # New type; not in spec
  
  def encode(date) do
    days = Date.to_gregorian_days(date)
    {@bson_type, <<days::unsigned-little-32>>}
  end
  
end

defmodule MyBeson do
  use Beson
  import_decoder(MyDateDecoder)
end

defmodule MyDateDecoder do
  @bson_type <<0x20>> # New type; not in spec
  
  def decode(buffer) do
    <<days::unsigned-little-32, rest::binary>> = buffer
    date = Date.from_gregorian_days(days)
    {date, buffer}
  end  
end

MyBeson.encode!(%{today: Date.utc_today()})
|> MyBeson.decode!()
%{"today" => ~D[2021-05-17]}