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.

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 install local npm package in a project

In some cases, like for testing purposes, you might want to use an npm package stored on a local machine. Here is how you can do that with one simple command.

Manually update Apollo cache after GraphQL mutation

Ensuring that GraphQL mutations properly update your Apollo client's cache can be a bit tricky - here's how to manually control that.

How to create and use custom shell commands?

Each of us had a situation, where we had to invoke a few, same commands each time. Making it once is not that problematic, but when we are supposed to repeat given operations, it may be better just to create one function that will handle it all.