Is it possible to determine whether a function head will match given arguments without executing the function body?
Here’s some example code to demonstrate the use case I’m trying to support. The following Calculator
module defines supported?/2
functions which return true if an operation is supported by the calculator. For each supported function there is a corresponding execute
function for the operation.
defmodule Calculator do
def reduce(ops, initial) do
ops
|> Enum.filter(fn {op, n} -> supported?(op, n) end)
|> Enum.reduce(initial, fn {op, n}, acc -> execute(op, n, acc) end)
end
defp supported?(:add, n) when is_number(n), do: true
defp supported?(:subtract, n) when is_number(n), do: true
defp supported?(_op, _n), do: false
defp execute(:add, n, acc) when is_number(n), do: acc + n
defp execute(:subtract, n, acc) when is_number(n), do: acc - n
end
Calculator.reduce([{:add, 10}, {:subtract, 5}, {:multiply, 2}], 0)
The problem with this code is the duplication between having both supported?
and execute
functions to ensure they are consistent (both functions exist, have same args, pattern matches, and guard clauses). This type of code is error prone because one must remember to modify both functions when making changes and is made worse because they may be physically separated when many different operations are supported.
Elixir has Module.defines?/3
which checks if the module defines a named function or macro of the given arity and kind (private, public function/macro). I’m after something similar, but which also takes arguments to check for a match, perhaps: Module.matches?(__MODULE__, {:function_name, 3}, :defp, [arg1, arg2, arg3])
.
With such a function the above example could be rewritten as:
defmodule Calculator do
def reduce(ops, initial) do
ops
|> Enum.filter(fn {op, n} -> supported?(op, n, initial) end)
|> Enum.reduce(initial, fn {op, n}, acc -> execute(op, n, acc) end)
end
defp supported?(op, n, initial) do
# Is there a matching `execute/3` function for the given args?
args = [op, n, initial]
Module.matches?(__MODULE__, {:execute, 3}, :defp, args)
end
defp execute(:add, n, acc) when is_number(n), do: acc + n
defp execute(:subtract, n, acc) when is_number(n), do: acc - n
end
Calculator.reduce([{:add, 10}, {:subtract, 5}], 0)
This replaces the manually defined and duplicated supported?
functions with checks to see whether a matching execute
function exists matching the given args.
Is there any existing way of acheiving this goal of having the supported?
function determined by the presence of a matching execute
function? One possible solution would be defining a macro which could implement the supported?/2
function under the covers by copying the args/guard clauses, but I’d prefer not to have to resort to a macro if possible.