So to preface, I'm a Ruby dev learning Elixir. I love to compare what I'm learning in Elixir back to what I know in Ruby, as I think that really strengthens my understanding of both languages. Today, I want to talk about function/method look up.
This article was prompted when I was learning about
GenServer through PragmaticStudio's course (I highly recommend it), and they mentioned that
GenServer can inject default implementations of functions if you do not define them manually. These default functions are:
And if you want to override them, just define a function in the current module with the same signature (name + arity).
But this got me thinking, how is it that Elixir look up function definition that makes this possible?
Before we jump into Elixir, let's first look at how method lookup in Ruby works.
I have actually blogged about this before, which was retweeted by Matz himself! (I actually have no idea how much this actually means, but I'm still going to wear it like a badge of honor)
Ruby's method lookup chain can literally be summarized in one method call -
Foo = Class.new Foo.ancestors # Lookup path for instance methods #=> [Foo, Object, Kernel, BasicObject] Foo.class.ancestors # Lookup path for class (singleton) methods, read my blog to learn more! #=> [Class, Module, Object, Kernel, BasicObject]
That's it -- that's how you see the entire method lookup chain for Ruby!
#ancestors is defined on
BasicObject itself, so when you call
Foo.ancestors, it asks this:
You: Do you implement `#ancestors`? - Foo: No - Object: No - Kernel: No + BasicObject: Yes I do!
And that's how it works!
With that, we know that if we want to
inject a function into Ruby, we just have to make sure that the newly injected function is the first method that gets found during the lookup (defining
#ancestors anywhere before
So, Ruby is able to do this because of
ancestors, but Elixir doesn't really have the concept of
inheritance, so what is really happening here?
It's about time to stop guessing, so I decided to try out an example myself.
defmodule Parent do defmacro __using__(_opts) do quote do def injected do "This is from Parent" end end end end defmodule Child do use Parent def injected do "This is from Child" end end
My expected behavior is that my
Parent.injected/0 will be overriden by my
Child.injected/0, but when I try to run this file -
iex lookup.exs, the following warning message pops up:
warning: this clause cannot match because a previous clause at line 12 always matches lookup.exs:14
Weird warning, but let's try to actually run it.
iex(1)> Child.injected "This is from Parent"
To my surprise (well the surprise is kinda ruined when I saw the warning message), running
Child.injected returns me
Parent.injected/0 instead of
I decided to dig into the doc and find out why, but the docs isn't really tell me much apart from the fact that
GenServer will inject functions for you. But I already knew that, I just want to know how is it doing so.
So, docs didn't help, what next?
As like most other languages, Elixir is open source, so I could very easily dive into the source code itself.
Now I see something suspiciously named
defoverridable, and I have a hunch that this might be it, but I want further proof to my hypothesis.
A quick Google about
defoverridable sent me to a post in Elixir Forum, written by the man himself, José Valim.
So through that post I found out that I can use
defoverridable, which would mean that I can update my code to the following:
defmodule Parent do defmacro __using__(_opts) do quote do def injected do "This is from Parent" end + defoverridable injected: 0 end end end defmodule Child do use Parent def injected do "This is from Child" end end
And now if we run it..
iex(1)> Child.injected "This is from Child"
Voilà it works! Perfect 🎉
In his post, José clarified
defoverridable for me, but he also went on to say that
defoverridable is not recommended, and they are in fact moving towards to
@optional_callback to mark methods as optional, along with an enhancement
@impl (which the compiler will use to make sure "child" implements that function), so that was also an interesting thing that I learned. I recommend you to read the post and comments to understand it a little more.
My query was literally answered in the first 10 lines of his post, so it would've been really nice if I managed to find it in the first place. Then again, if I hadn't went into the source code I wouldn't have known what keyword to look for, and thus won't land at that post.
Still, an interesting journey for me because at one point I thought Elixir had inheritance like Ruby, heh.
(also, title is technically incorrect as Elixir does not really have "function lookup" - all it does is just look at the current module!)