Using pattern matching to provide "meta-information" about function

Hello,

I am working on an interpreted language that has a type checker.
I have a pattern structure like this:

    mult = %OpType{
      operation: "*",
      return_type: :num,
      fields: %{
        "~a" => :num,
        "~b" => :num
      },
      execute: fn(%{"~a" => a, "~b" => b}) ->  a * b end
    }

What I want to do is interpret a string. Everything is working for me, except that I can’t make this structure a module attribute because of the embedded function.

Essentially the “compiler” looks like this:

# usage: compile("*", %{"~a" => 1, "~b" => 5}, %{ "*" => %OpType{...}, "+" => %OpType{...}, ... })
def compile(operation, params, all_operations) do
    procedure = all_operations[operation]
    compiled_procedures = for {param_key, expected} <- Map.to_list(procedure.fields), into: %{} do
      resolved = resolve(params, param_key, expected)
      {param_key, compile(resolved, all_operations)}
    end
    fn(refs) ->
      params = for {param_key, compiled} <- Map.to_list(compiled_procedures), into: %{} do
        {param_key, fnc.(refs)}
      end
      procedure.execute.(params)
    end
  end

I’m passing around all_operations because I can’t figure out how to make a global from it.

My type information (to statically check the AST) was stored next to the execute method. It was very handy having all the functionality for a method defined in one place.

But I found out that it is much more efficient to use pattern matching rather than looking up the operation in a map.

def compile(%Token{token: "+", params: params}) do
    compiled_a = compile(resolve(params, "~a", :num))
    compiled_b = compile(resolve(params, "~b", :num))
    fn(refs) ->
      a = compiled_a.(refs)
      b = compiled_b.(refs)
      a + b
    end
  end
  def compile(%Token{token: "*", params: params}) do
    compiled_a = compile(resolve(params, "~a", :num))
    compiled_b = compile(resolve(params, "~b", :num))
    fn(refs) ->
      a = compiled_a.(refs)
      b = compiled_b.(refs)
      a * b
    end
  end

But now the type information I have for the compile-time resolution of the variable, is now separate from the structs that the type checker uses.

What I am thinking about instead is doing this:

defmodule Prelude do
  def add(:return_type), do: :num
  def add(:type, "~a"), do: :num
  def add(:type, "~b"), do: :num
  def add(:execute, a, b), do: a + b
end

...

def compile(%Token{token: "+", params: params}) do
    compiled_a = compile(resolve(params, "~a", Prelude.add(:type, "~a")))
    compiled_b = compile(resolve(params, "~b", Prelude.add(:type, "~b"))
    fn(refs) ->
      a = compiled_a.(refs)
      b = compiled_b.(refs)
      Prelude.add(:execute, a, b)
    end
end

...
 %OpType{
      operation: "+",
      return_type: Prelude.add(:return_type),
      fields: %{
        "~a" => Prelude.add(:type, "~a"),
        "~b" => Prelude.add(:type, "~b")
      },
      execute: &Prelude.add/3
}

This way I can extract meta information from the method and keep the type information grouped with the execution logic.

The problem is that this seems a little odd to me and I don’t normally see elixir functions mixing concerns based on overloading.

I was wondering if anyone had any suggestions for how to organize this better. Or if others think this is kosher.

Ideally I would like to find a way to make the OpType struct global and then I could just call

def compile("+", params) do
      ...
      add.execute.(a, b)
      ...
end

Another Option would be to have a behavior:

defmodule Multiply do
  @behavior Operation
  def token(), do: "*"
  def return_type(), do: :num
  def fields(), do: ["~a", "~b"]
  def type("~a"), do: :num
  def type("~b"), do: :num
  def execute(a, b), do: a * b
end

This might make the most sense.