DEV Community

Cover image for Function/Method look up in Elixir/Ruby
Edison Yap
Edison Yap

Posted on

Function/Method look up in Elixir/Ruby

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] 
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And now if we run it..

iex(1)> Child.injected
"This is from Child"
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
thiagoa profile image
Thiago Araújo Silva

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 call super!

Collapse
 
cescquintero profile image
Francisco Quintero 🇨🇴

Excellent article. Came to learn a bit about Elixir and learnt also about Ruby 😅😅