On the surface

Elixir and Ruby look alike. This is very much understandable, since José Valim was involved in Ruby ecosystem before he switched to become a language designer and maintainer. Both languages are strongly and dynamically typed, share similar method/function/module definition syntax, and overall layout of source files is similar.

Digging deeper

Despite the surface similarities, the two languages do not share much more than the syntax. Elixir is functional, Ruby - object-oriented. Elixir does not allow mutable state, in Ruby it’s all over the place.

When I started writing more Elixir code, I found that the overall workflow differs too. I tend to catch more bugs at runtime in Ruby programs. When I write Elixir, the bugs are often caught at the compilation phase. Most of those bugs are related to invalid function arity, or attempting to call functions that do not exist (typos, missing imports etc.).

Compilation

First step before running Elixir program is to compile it. Ruby does have the compilation phase as well. It’s not much different, although it’s hidden from the developer during the normal workflow. Before Ruby’s program is executed, source code is converted to AST, then to bytecode. Only if those phases succeed, program is executed. In both languages the same basic principle applies.

And yet - I tend to catch more errors during compilation of Elixir than of Ruby code. Why?

Function/method calls

Ruby borrows a lot from Smalltalk. It’s object’s model is virtually clone of the object model in Smalltalk. The same is true about method dispatch.

In fact, in Ruby we often talk about sending messages to objects. obj.foo() is could be very well written as obj.send(:foo). The method dispatch in Ruby works by sending messages consisting of method name and a list of parameters to given objects. If the object implements given method, it handles it and responds. If it does not - the message is passed up in the inheritance chain until is handled by code implemented in one of the ancestors. The method can also be not found anywhere in the parent classes of the object it was dispatched to. In such case, another cycle is started - now in the search of special method called method_missing. Fair share of Ruby’s metaprogramming capabilities are due to this smart - yet not very performant - method dispatch algorithm.

On the surface, Elixir’s function calls code is as relaxed as Ruby’s method dispatch. If you write the following code, it will compile properly, and only throw an error at runtime:

defmodule Foo do
  def foo(_a, _b, _c) do
    IO.puts "Foo.foo reporting for duty"
  end
end

defmodule Bar do
  def call_foo do
    Foo.foo()
  end
end

Let’s compile and run it:

➜  mix compile
Compiled lib/foobar.ex
➜  mix run -e "Bar.call_foo()"
** (UndefinedFunctionError) undefined function Foo.foo/0

This looks equally bad as in Ruby. However, you don’t usually write code like that. Instead, you would most likely use import mechanism. Let’s amend the 2nd module definition to reflect that:

defmodule Bar do
  import Foo, only: [foo: 0]

  def call_foo do
    foo()
  end
end

This gives us helpful compilation error:

➜  mix compile
== Compilation error on file lib/foobar.ex ==
** (CompileError) lib/foobar.ex:8: cannot import Foo.foo/0 because it
doesn't exist

Moreover, if we import the function with proper arity, yet try call the function named the same yet with different arity, we’ll get similar error:

defmodule Bar do
  import Foo, only: [foo: 3]

  def bar do
    foo()
  end
end

produces compilation error as well:

➜  mix compile
== Compilation error on file lib/foobar.ex ==
** (CompileError) lib/foobar.ex:11: undefined function foo/0

This suggests us that importing a function before executing it, not only releases us from the obligation to prefix it with module name - but makes our code safer since we will catch missing function and arity errors at compile time.

Having said the above, you probably should not import all functions before calling them. It makes it difficult to distinguish between local and external functions.

When we work with functions defined in the current module, we get the benefit of compilation errors by default. Consider the following code:

defmodule Bar do
  def other_function(_a) do
  end

  def bar do
    other_function()
  end
end

Let’s compile it:

➜  mix compile
== Compilation error on file lib/foobar.ex ==
** (CompileError) lib/foobar.ex:12: undefined function

other_function/0

XREF on Elixir >= 1.3.0

Elixir 1.3 brings new exciting compile-time checks. When you try to use external functions from other modules, that either do not exist or are bad arity, you will get compilation-time warnings. Note that this will warn you:

IO.pssssstt "This does not exist"

but this will not:

mod = IO
mod.pssssst "This does not exist but does not warn at compile time!"

So yeah, passing around modules in variables may lead to some tricky to debug issues.

Metaprogramming

Both languages come with strong metaprogramming capabilities. Yet in Ruby, in order to catch a bug in “code that writes code”, you have to run it.

In Elixir, main mechanism to implement metaprogramming - is to use macros. Macros are executed at compile time. If the macro contains a bug, it is quite likely the compilation fails. Let’s consider the following:

defmodule Foo do
  defmacro make_fun(name) do
    quote do
      def unquote(:"#{name}")() do
        print(unquote(name))
      end
    end
  end
end

defmodule Bar do
  require Foo

  Foo.make_fun("foo")

  def print(name) do
    IO.puts name
  end
end

The above simple macro will create Bar.foo/0 function for us. Let’s try it:

➜  mix compile
Compiled lib/foobar.ex
Generated foobar app
➜  mix run -e "Bar.foo()"
foo

All looks good. Now, let’s make an error in macro definition by using puts as function name instead of print:

defmodule Foo do
  defmacro make_fun(name) do
    quote do
      def unquote(:"#{name}")() do
        puts(unquote(name))
      end
    end
  end
end

Let’s compile it:

➜  foobar  mix compile
== Compilation error on file lib/foobar.ex ==
** (CompileError) lib/foobar.ex:14: undefined function puts/1

Compile-time error! Good.

In reality, you more often use macros than write them from scratch. But the benefits of the compilation errors often surface anyway, since most macros are somehow dependent on the structure of the code in module you are using the macro in. It may be invalid options passed to macro, not implemented functions or other sort of problems that will surface as soon as you attempt to use macro at compilation time.

Why compilation-time errors are a good thing?

The earlier you get the feedback about a bug, the better. If the error is caught at runtime, you do need to make sure you actually execute the code before you push it to production. This means writing more extensive tests or do more manual checks.

By doing more checks at compile time, and expecially because of it’s completely different metaprogramming system, Elixir allows me to catch more bugs earlier.

Post by Hubert Łępicki

Hubert is partner at AmberBit. Rails, Elixir and functional programming are his areas of expertise.