Skip to content

Instantly share code, notes, and snippets.

@fredguth
Last active November 8, 2025 13:17
Show Gist options
  • Select an option

  • Save fredguth/0495653fa1ab662d5bb7f7b20c2245ce to your computer and use it in GitHub Desktop.

Select an option

Save fredguth/0495653fa1ab662d5bb7f7b20c2245ce to your computer and use it in GitHub Desktop.
Patch in elixir

Building a Patch System for Elixir Livebook

Mix.install([
  {:kino, "~> 0.17.0"}
])

Run in Livebook

Motivation

When developing in Jupyter notebooks with Python's fastcore, we can use the @patch decorator to incrementally add methods to a class across multiple cells. This creates a natural development narrative where we can see the evolution of our code.

In Elixir with Livebook, we face a challenge: modules are compiled as complete units, and Livebook prevents redefining a module in subsequent cells. Let's build a Patch system that gives us a similar iterative development experience.

Setup

Mix.install([
  {:kino, "~> 0.17.0"}
])

The Problem

Let's see what happens when we try to redefine a module in Livebook:

defmodule ModA do
  def add(a, b), do: a  # incorrect on purpose
end
warning: variable "b" is unused (if the variable is not meant to be used, prefix it with an underscore)
└─ Code/elixir/patch.livemd#cell:wzxny5ohqko6oh7y:2: ModA.add/2

{:module, ModA, <<70, 79, 82, 49, 0, 0, 6, ...>>, {:add, 2}}

If we try to redefine ModA in another cell, we get a compile error. Livebook won't allow it because it checks for duplicate module definitions at compile time.

Certainly you can come back to the cell and fix it, but sometimes, doing so prevent you to record your journey.

The Solution: Dynamic Module Building

Our approach will be to:

  1. Store function definitions as quoted AST in an Agent
  2. Accumulate these definitions across cells
  3. Rebuild the entire module dynamically using Code.compile_quoted/1

Let's build the Patch module:

defmodule Patch do
  
  # Keep accumulated quoted defs per module in an Agent
  def start_link do
    Agent.start_link(fn -> %{} end, name: __MODULE__)
  end

  # Add a quoted block of definitions to a module's stash
  def to(mod, quoted_block, opts \\ []) when is_atom(mod) do
    # Auto-start the Agent if not running
    unless Process.whereis(__MODULE__), do: start_link()
    
    # Prepend new block (so newest definitions come first and win)
    Agent.update(__MODULE__, fn acc ->
      Map.update(acc, mod, [quoted_block], fn blocks -> [quoted_block | blocks] end)
    end)
    
    # Auto-rebuild unless explicitly disabled
    if Keyword.get(opts, :rebuild, true), do: build(mod)
  end

  # Compile the module from accumulated blocks
  def build(mod) when is_atom(mod) do
    blocks = 
      Agent.get(__MODULE__, fn acc ->
        Map.get(acc, mod, [])  
      end)
      
    # Build the module AST and compile it
    quote do
      defmodule unquote(mod) do
        unquote_splicing(blocks)
      end
    end
    |> Code.compile_quoted()
    
    mod
  end

  # Get the source code of accumulated functions
  def get_source(mod, opts \\ []) when is_atom(mod) do
    Agent.get(__MODULE__, fn acc ->
      Map.get(acc, mod, [])  
    end)
    |> then(fn blocks ->
      if Keyword.get(opts, :final, false) do
        # Show only final definitions (deduplicated)
        Enum.uniq_by(blocks, &get_signature/1)
      else
        # Show full history
        blocks
      end
    end)
    |> Enum.map_join("\n\n", &Macro.to_string/1)
  end

  # Extract function name and arity for deduplication
  defp get_signature({:def, _, [{name, _, args}, _]}) when is_list(args), do: {name, length(args)}
  defp get_signature({:def, _, [{:when, _, [{name, _, args}, _]}, _]}) when is_list(args), do: {name, length(args)}
  defp get_signature(other), do: other

  # Display source code with nice formatting
  def display_code(mod, opts \\ []) when is_atom(mod) do
    source = get_source(mod, opts)
    
    if Code.ensure_loaded?(Kino.Markdown) do
      "```elixir\n#{source}\n```"
      |> Kino.Markdown.new()
    else
      IO.puts(source)
      source
    end
  end
end
{:module, Patch, <<70, 79, 82, 49, 0, 0, 24, ...>>, {:display_code, 2}}

I wish we could have built this with Patch, because I did it piece by piece.

Using Patch: Iterative Development

Now let's use our Patch system to build a module incrementally:

# Start with an incorrect implementation
Patch.to(ModA, quote do
  def add(a, b), do: a  # oops, forgot to add b!
end)
ModA
# Test it - we'll see the bug
ModA.add(5, 6)
5
# Add another function
Patch.to(ModA, quote do
  def mul(a, b), do: a * b
end)
ModA
# Check what functions we have
ModA.__info__(:functions)
[add: 2, mul: 2]

The Power of Redefinition

Here's where it gets interesting - we can fix our buggy add/2 function:

# Fix the add function by redefining it
Patch.to(ModA, quote do
  def add(a, b), do: a + b
end)
ModA
# Now it works correctly!
ModA.add(5, 6)
11

Why does this work? In Elixir, when multiple definitions of the same function exist, the first one wins. Our Patch system prepends new definitions, so the latest version comes first and takes precedence.

Viewing the Source

We can view the accumulated source code in two ways:

Patch.get_source(ModA)
"def add(a, b) do\n  a + b\nend\n\ndef mul(a, b) do\n  a * b\nend\n\ndef add(a, b) do\n  a\nend"
# Show the full development history
Patch.display_code(ModA)
# Show only the final, deduplicated version
Patch.display_code(ModA, final: true)

Key Features

Our Patch system provides:

  1. Incremental development - Add functions one at a time across cells
  2. Function redefinition - Fix bugs by redefining functions in new cells
  3. Development narrative - Keep the full history visible in your notebook
  4. Source inspection - View accumulated code with get_source/2
  5. Auto-initialization - No need to manually start the Agent
  6. Deferred building - Option to accumulate multiple functions before rebuilding

Conclusion

While we can't truly "patch" modules in Elixir like we can in Python, we've created a system that provides a similar iterative development experience in Livebook. The key insight is to store function definitions as AST and rebuild the entire module dynamically, giving us the flexibility to develop incrementally while maintaining the narrative flow of our notebook.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment