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:
init/1
child_spec/1
code_change/3
terminate/2
handle_info/2
handle_cast/2
handle_call/3
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.
Your dad, your dad's dad, and your dad's dad's dad.
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 - ancestors
.
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!
In fact, #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 BasicObject
).
So, Ruby is able to do this because of ancestors
, but Elixir doesn't really have the concept of ancestors
/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 Child.injected/0
, why?!
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?
Source Code!
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.
Solution
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 🎉
Conclusion
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!)
Top comments (2)
Nice post! But from what I understand, he did not say
defoverridable
is not recommended. He said people were abusing it to define unnecessary default implementations or functions not backed by a behaviour. I guess using it to simulate an OO environment is one of the problems. You can even callsuper
!Excellent article. Came to learn a bit about Elixir and learnt also about Ruby 😅😅