Kamil Lelonek
Kamil Lelonek

Reputation: 14744

How to create "abstract" module in Elixir or "virtual" functions?

I know I'm a little bit Ruby-ish or even Java-ish here but here's the case I encounter very often.

I want to define some general module (metaprogramming can be used) which will use some variables/function that will be specified in "children" modules (which will probably use this general module).

I imagine it like:

defmodule Parent do
  defmacro __using__(_) do
    quote do
      def function do
        __MODULE__.other_function
      end
    end
  end
end

and later on:

defmodule Child do
  use Parent

  def other_function do
    # some real work
  end
end

I very often need to "abstract" some function and the best it would be by defining some module-variable @var accessible in "parent" module but I know that it doesn't work like that whatsoever.

Is there any way to call a function that will be defined in "including" module?

Upvotes: 3

Views: 882

Answers (2)

Alex de Sousa
Alex de Sousa

Reputation: 1591

I think you're on the right track. If you add a @callback to define the spec for the "abstract" function it would be better. I'll explain with a working example:

defmodule Fibonacci do
  @callback fib(number :: integer) :: integer

  defmacro __using__(_) do
      quote do
        @behaviour Fibonacci

        def fibonacci(n) when not is_integer(n), do: {:error, "Not integer"}
        def fibonacci(n) when n < 0, do: {:error, "Negative number"}
        def fibonacci(n), do: {:ok, __MODULE__.fib(n)}

        def fibonacci!(n) do
          case fibonacci(n) do
            {:error, reason} -> raise reason
            {:ok, n} -> n
          end
        end
      end
  end
end

Every module that uses Fibonacci will need to implement the callback function fib/1. Also every module that use the Fibonacci module will have the function fibonacci/1 that handles errors and fibonacci!/1 that raises errors.

So, let's implement fib/1 with direct recursion:

defmodule Direct do
  use Fibonacci

  def fib(0), do: 0
  def fib(1), do: 1
  def fib(n), do: fib(n - 1) + fib(n - 2)
end

And for tail recursion:

defmodule Tail do
  use Fibonacci

  def fib(0), do: 0
  def fib(1), do: 1
  def fib(n), do: fib(1, 1, n - 2)

  defp fib(_, value, 0), do: value
  defp fib(n_2, n_1, n), do: fib(n_1, n_2 + n_1, n - 1)
end

So in iex we can call the different implementations:

iex(1)> Direct.fibonacci(10)
{:ok, 55}
iex(2)> Tail.fibonacci!(10)
55
iex(3)> Tail.fibonacci!(-10)
** (RuntimeError) Negative number

Also, if you forget to define the fib/1 function in your module, then the compiler will warn you about it:

defmodule Warning do
  use Fibonacci
end

* warning: undefined behaviour function fib/1 (for behaviour Fibonacci)

If you want to play with this code, just follow this link http://elixirplayground.com?gist=606fe8c283443f03c7af08f85c888fe3

I hope this answers your question.

Upvotes: 3

tkowal
tkowal

Reputation: 9289

There is probably more than one way to do it depending on your exact use case. The simplest thing to do is to just use higher order functions and no metaprogramming at all.

defmodule Parent do
  def function(function_passed_as_variable) do
    function_passed_as_variable.()
  end
end

defmodule Child do
  def function do
    Parent.function(&other_function/1)
  end
  def other_function do
    #some real function
  end
end

I kept the names Parent and Child to refer to your code, but in this situation it doesn't fit.

To achieve polymorphism (one function doing different things depending on data), you could use pattern matching:

def function({:first_type, data}), do: data*2
def function({:second_type, data}), do: data+2
def function(data), do: data

There are also protocols which can be defined for different structs and other datatypes.

It might not be easy to choose the right pattern so feel free to comment on more specific example.

Upvotes: 2

Related Questions