Adding/Modifying module functions in a previous Livebook cell

I would like to use Livebook to demonstrate building up an increasingly complex module.
Is it possible to add functions without re-specifying the full module in later code cells?

Eg.

defmodule Example do
  def add(a, b) do
    a + b
  end
end
Example.add(1, 2)

3

defmodule Example do
  def sub(a, b) do
    a - b
  end
end
Example.add(1, 2)

** (UndefinedFunctionError) function Example.add/2 is undefined or private
Example.add(1, 2)

Did you ever figure out if this is possible?

Not so far, I haven’t been working on this project for the last few week. I might return to it if there isn’t a solution already at the end of the year.

I wrote a module that will handle what you’re looking for with only a little extra code:

defmodule AndAlso do
  defmacro __using__(arg) do
    quote bind_quoted: [module: arg] do
      module
      |> AndAlso.to_delegate()
      |> Enum.each(fn {name, arity} ->
        fun = AndAlso.make_fun(name, arity)
        defdelegate unquote(fun), to: module
        defoverridable [{name, arity}]
      end)
    end
  end

  def to_delegate(module) do
    module.module_info(:exports)
    |> Enum.reject(&is_underscored?/1)
    |> Enum.reject(&is_module_info?/1)
  end

  defp is_underscored?({name, _arity}) do
    name
    |> Atom.to_string()
    |> String.starts_with?("__")
  end

  defp is_module_info?({name, _arity}) do
    name == :module_info
  end

  def make_fun(name, arity) do
    {name, [], make_args(arity)}
  end

  defp make_args(arity) do
    Enum.map(1..arity, &{String.to_atom("x#{&1}"), [], Elixir})
  end
end

then you use it like this:

defmodule Example do
  def add(a, b), do: a + b
end

defmodule Example2 do
  use AndAlso, Example

  def sub(a, b), do: a - b
end

Example2.add(1, 2)
# => 3

defmodule Example3 do
  use AndAlso, Example2

  def add(a, b), do: a + 2*b
end

Example3.add(1, 2)
# => 5

Some things to be aware of:

  • I have no idea what the implications of doing this for performance are. They’re probably not huge, but measure them before using this in anything besides a Livebook

  • using AndAlso on modules that define functions prefixed with __ will not work like you expect. For instance, a module that calls defstruct or says use Ecto.Schema

  • while you’re normally allowed to redefine modules, the way this macro generates code requires that “old” versions have distinct names - doing this will not work:

defmodule Example do
  def add(a, b), do: a + b
end

defmodule Example do
  use AndAlso, Example

  def sub(a, b), do: a - b
end

it fails with (on 1.14-otp-25):

** (ArgumentError) defdelegate function is calling itself, which will lead to an infinite loop. You should either change the value of the :to option or specify the :as option
    (elixir 1.14.0) lib/kernel/utils.ex:32: Kernel.Utils.defdelegate_all/3
    iex:87: anonymous fn/1 in :elixir_compiler_10.__MODULE__/1
    (elixir 1.14.0) lib/enum.ex:975: Enum."-each/2-lists^foreach/1-0-"/2
    iex:87: (module)
    (elixir 1.14.0) src/elixir_compiler.erl:65: :elixir_compiler.dispatch/4
    (elixir 1.14.0) src/elixir_compiler.erl:50: :elixir_compiler.compile/3
    (elixir 1.14.0) src/elixir_module.erl:379: :elixir_module.eval_form/6
    iex:86: (file)
3 Likes

Thanks for that.

I just pushed up an experiment I did towards this goal. It stores code in ETS then stitches it together, but it requires you to include a version with your module definition.

import ModulePatching, only: [defmodule: 3]
ModulePatching.start()
defmodule Example, 1 do
  def add(a, b) do
    a + b
  end
end
Example.add(1, 2)
#=> 3
defmodule Example, 2 do
  def sub(a, b) do
    a - b
  end
end
Example.add(1, 2)
#=> 3
2 Likes