Metaprogramming using a data source

I’m building a module from a csv. If I set it up to define methods as I parse through the file, it works fine:

defmodule ExModbus.SunSpec.Fronius do
  @external_resource Path.join([__DIR__, "sunspec.csv"])
  for line <- File.stream!(Path.join([__DIR__, "sunspec.csv"]), [], :line) |> Stream.drop(1) do
    [start, _end, size, _access, _func_codes, name, desc, type, _units, _scale_factor, _range] = String.split(line, ",")

    start = String.to_integer(start)
    size = String.to_integer(size)
    name = String.to_atom(String.downcase(name))

    def unquote(name)(pid, slave_id \\ 1) do
      case ExModbus.Client.read_data(pid, slave_id, unquote(start - 1 + 40000), unquote(size)) do
        %{data: {:read_holding_registers, data}, transaction_id: transaction_id, unit_id: unit_id} ->
          data = data |> ExModbus.Types.convert_type(unquote(type))
          {:ok, %{data: data, transaction_id: transaction_id, slave_id: unit_id}}
        other ->
          IO.puts inspect other
      end
    end
  end
end

However, my CSV also includes a description, and I want to use it for documentation @doc unquote(desc) This doesn’t work though, because I’m not in a quote block. So I tried this:

defmodule ExModbus.SunSpec.Fronius do
  @external_resource Path.join([__DIR__, "sunspec.csv"])
  ast = for line <- File.stream!(Path.join([__DIR__, "sunspec.csv"]), [], :line) |> Stream.drop(1) do
    [start, _end, size, _access, _func_codes, name, desc, type, _units, _scale_factor, _range] = String.split(line, ",")

    start = String.to_integer(start)
    size = String.to_integer(size)
    name = Macro.underscore(name)

    quote do
      def unquote(name)(pid, slave_id \\ 1) do
        case ExModbus.Client.read_data(pid, slave_id, unquote(start - 1 + 40000), unquote(size)) do
          %{data: {:read_holding_registers, data}, transaction_id: transaction_id, unit_id: unit_id} ->
            data = data |> ExModbus.Types.convert_type(unquote(type))
            {:ok, %{data: data, transaction_id: transaction_id, slave_id: unit_id}}
          other ->
            IO.puts inspect other
        end
      end
    end
  end

  IO.puts inspect Macro.to_string(ast)
  Code.eval_quoted ast
end

My AST looks reasonable, but it dies at the last line with:

** (ArgumentError) cannot invoke def/2 outside module
(elixir) lib/kernel.ex:4436: Kernel.assert_module_scope/3
(elixir) lib/kernel.ex:3433: Kernel.define/4
(elixir) expanding macro: Kernel.def/2
nofile:1: (file)

It looks like I’m running this in a module, but apparently not.

The docs say Code.eval_quoted is bad form anyway, so I tried replacing it with this:

  quote do
    unquote(ast)
  end

This compiles, but none of my methods are defined. It seems wrong that my AST is a list, and not a tree, but the same is true for the Translator module from Metaprogramming Elixir, and it’s still working. I suppose this isn’t the top of the tree anyway. But I’m out of guesses for the moment. Any suggestions?

1 Like

s/methods/functions :slight_smile:

You can set the @doc attribute simply inline before your function def:

defmodule ExModbus.SunSpec.Fronius do
  @external_resource Path.join([__DIR__, "sunspec.csv"])
  for line <- File.stream!(Path.join([__DIR__, "sunspec.csv"]), [], :line) |> Stream.drop(1) do
    [start, _end, size, _access, _func_codes, name, desc, type, _units, _scale_factor, _range] = String.split(line, ",")

    start = String.to_integer(start)
    size = String.to_integer(size)
    name = String.to_atom(String.downcase(name))

    @doc "Function that does #{desc}"
    def unquote(name)(pid, slave_id \\ 1) do
      case ExModbus.Client.read_data(pid, slave_id, unquote(start - 1 + 40000), unquote(size)) do
        %{data: {:read_holding_registers, data}, transaction_id: transaction_id, unit_id: unit_id} ->
          data = data |> ExModbus.Types.convert_type(unquote(type))
          {:ok, %{data: data, transaction_id: transaction_id, slave_id: unit_id}}
        other ->
          IO.puts inspect other
      end
    end
  end
end
3 Likes

Ha! Looks like I was mentally checked out yesterday evening. Thanks Chris!

2 Likes