How to override Kernel macros

Article autor
September 9, 2025
How to override Kernel macros
Elixir Newsletter
Join Elixir newsletter

Subscribe to receive Elixir news to your inbox every two weeks.

Oops! Something went wrong while submitting the form.
Elixir Newsletter
Expand your skills

Download free e-books, watch expert tech talks, and explore open-source projects. Everything you need to grow as a developer - completely free.

Table of contents

The macro mechanism in Elixir is not only an interesting metaprogramming feature - in fact, it is at the language's very core. And the more awesome fact is that, using macros, you can override the algorithm of defining functions itself!

This TIL is a build-up for Michal Buszkiewicz's ElixirConf EU 2021 talk on debugging in Elixir - see you in Warsaw on Sep 10, 2021!

Elixir is a language built upon macros, that are used to transform code into different code. Some of the language's keywords such as case , cond , etc. are actually implemented at the compiler level and are shimmed with the Kernel.SpecialForms module, whereas others such as raise , if , unless etc. are implemented as macros that do their job interacting with Erlang-provided modules as well as those from Kernel.SpecialForms .

Hell, even def is defined as a macro and to make things even weirder, so is defmacro itself (explained simply enough here).

One thing that you have to keep in mind before you go further is that Kernel and Kernel.SpecialForms is imported automatically into each of your custom modules. You can, however, opt to use the except option to skip certain macros:

defmodule NoIf do
  import Kernel, except: [if: 2]

  def foo() do
    # trying to use if/2 will result in an error
  end
end

That's just an example. How can we use it for more practical purposes? We can, for instance, override def to tamper with every function that we implement inside the module - for whatever purposes (logging function calls, other debugging purposes, etc.).

defmodule CustomDef do
  defmacro def(call, expr \\ nil) do
    IO.inspect([call: call, expr: expr], label: "Defining")

    quote do
      Kernel.def(unquote(call), unquote(expr))
    end
  end
end

defmodule NoDef do
  import Kernel, except: [def: 2, def: 1, defp: 2, defp: 1]
  import CustomDef

  def foo(arg) do
    {:ok, :foo, arg * 2}
  end
end

IO.inspect(NoDef.foo(2), label: "Inside NoDef.foo/0")
IO.inspect(NoDef.foo(3), label: "Inside NoDef.foo/0")

The output is:

Defining: [
  call: {:foo, [line: 15], [{:arg, [line: 15], nil}]},
  expr: [
    do: {:{}, [line: 16],
     [:ok, :foo, {:*, [line: 16], [{:arg, [line: 16], nil}, 2]}]}
  ]
]
Inside NoDef.foo/0: {:ok, :foo, 4}
Inside NoDef.foo/0: {:ok, :foo, 6}

For demonstration purposes, I wanted to show that we were able to put an output message at the stage of defining the function, and - as you can see that expr contains the function's AST - you can alter it so that, for instance, every function you create in your module contains a logger call at the beginning.

For easier usage in any of your modules, you could create a __using__ macro so you can plug it in with the use keyword.

How to check if a set contains exact values with Jest in JS?

Related posts

Dive deeper into this topic with these related posts

No items found.

You might also like

Discover more content from this category

How to call useEffect React Hook on a component mount and unmount

With pure function React Components you're not allowed to use lifecycle methods like componentDidMount or componentWillUnmount.

Using Logger.info and Logger.debug in ExUnit tests

By default in the test env, Phoenix doesn't show Logger.debug/Logger.info outputs in the console.

How to revert commit in Git

Did you ever create a commit that you wish never happened? Let's be honest - we all did. There is an easy way to revert it in Git.