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.