For those who don’t know, Elixir is a programming language that took a lot of inspiration from Clojure, and thus shares a lot of similarities with Clojure, while also taking some inspo from Erlang and Ruby. I’m fond of it, even though I prefer Clojure.
One pattern they have in Elixir I find is very interesting in my opinion. They basically create a form of inheritance of namespaces (which is called a module in Elixir), by simply leveraging a macro that a namespace can expose which when called from another namespace will return a series of require
, imports
, def
, defn
, defmacro
, etc. Thus inheriting all those inside the deriving namespace.
In their web framework of choice, Pheonix, they make extensive use of this in order to create that “easy” framework feel that sometimes you get with OOP frameworks.
I’ve re-created it in Clojure, so this will help you understand as well what the pattern is:
(ns foo)
(defmacro __inherit__
[& {:as _opts}]
(quote
(defn baz []
(println "Hello Foo"))))
(ns app
(:require [inheritance :refer [inherit]]))
(inherit foo)
(baz)
;;=> Hello Foo
As you can see, a namespace such as foo
in this example exposes a “template” of some sort by defining a __inherit__
macro. Everything it returns will be evaluated in the scope of the namespace that “inherits” from it, as if you had typed that code directly into it.
In Elixir it looks like this:
defmodule Foo do
defmacro __using__(_opts) do
quote do
def baz() do
IO.puts("Hello Foo")
end
end
end
end
defmodule App do
use Foo
end
App.baz()
#=> Hello Foo
They call it __using__
and use
where I chose to rename it so it doesn’t conflict with Clojure’s existing use
core function.
Another feature which gives it more of a “template” vibe than your standard inheritance is that you can pass it options to customize the “inheritance” behavior:
(ns bar
(:require [inheritance :refer [binded-quote]]))
(defmacro __inherit__
[& {:as opts}]
(binded-quote
[name (:name opts)]
(defn baz []
(println "Hello" name))))
(ns app
(:require [inheritance :refer [inherit]]))
(inherit bar :name "Jane Doe")
(baz)
;;=> Hello Jane Doe
In Elixir it looks like:
defmodule Bar do
defmacro __using__(opts) do
quote do
def baz() do
IO.puts("Hello #{unquote(opts[:name])}")
end
end
end
end
defmodule App do
use Bar, name: "Jane Doe"
end
App.baz()
#=> Hello Jane Doe
And it doesn’t just inherit functions, think of it as all the code inside __inherit__
is being copy/pasted in your namespace, so it can be used to set common aliases, imports, requires, constants and all that:
(ns foo)
(defmacro __inherit__
[& {:as _opts}]
(quote
(do
(require '[clojure.string :refer [join]])
(require '[clojure.math :as m])
(def pi 3.14159265359)
(defn baz []
(println "Hello Foo")))))
(ns app
(:require [inheritance :refer [inherit]]))
(inherit foo)
(baz)
;;=> Hello Foo
(join "," [1 2 3 4])
;; => "1,2,3,4"
(m/pow 2 pi)
;; => 8.824977827077554
And you can “override” things by just re-def-ing them:
(ns foo)
(defmacro __inherit__
[& {:as _opts}]
(quote
(defn baz []
(println "Hello Foo"))))
(ns app
(:require [inheritance :refer [inherit]]))
(inherit foo)
(defn baz []
(println "Hello World"))
(baz)
;;=> Hello World
There can be clashes if you “inherit” more than one, in which case last one wins, so the order in which you inherit matters in those cases.
inheritance.clj
(ns inheritance
(:require [clojure.walk :as walk]))
(defn binded-quote*
[bindings form]
(walk/postwalk-replace (apply hash-map bindings) form))
(defmacro binded-quote
[bindings form]
`(binded-quote* ~(->> (partition 2 bindings)
(mapcat (fn[[f l]] [`'~f l]))
(vec)) '~form))
(defmacro inherit
[nmsp & {:as opts}]
(let [inrt (symbol (str nmsp) "__inherit__")]
`(do
(requiring-resolve '~inrt)
(~inrt ~opts))))
I’m curious what people think of this pattern, is it madness, or is it genius?