The Versatility of Function Declarations in Elixir
# ruby
def hello
puts "Hello world!"
end
# elixir
def hello do
IO.puts("Hello world!")
end
Over the past few months I’ve been lucky enough to use Elixir at work. I wrote some hobby projects in Ruby in the past, so my first surprise was in finding out how differently Elixir behaves despite the syntactic similarities between the two.
The sameness pretty much ends there. Ruby is an interpreted object-oriented language. Elixir on the other hand is a compiled, functional language which runs on the Erlang VM. In Elixir, functions are first-class citizens defined by two properties: name & arity.
Name is self-explanatory, but you may not have come across arity: The arity of a function is the number of arguments it takes.
# hello/0
def hello do
IO.puts("Hello world!")
end
# hello/1
def hello(name), do: IO.puts("Hello #{name}!")
hello() # Hello world!
hello("George") # Hello George!
While these share the same name, for all intents and purposes they behave like different functions. Defining multiple function clauses like this can get verbose, but we can utilise the alternate def hello(name), do:
syntax to declare functions in a single line and keep things terse.
Using multiple function clauses can remove the need for what would typically be branching logic inside a single function definition. The utility in this becomes really evident when you also leverage Elixir’s pattern matching.
Let’s refactor hello/1
to address a few edge cases:
# hello/1
def hello(nil), do: IO.puts("..?")
def hello("Jerry"), do: IO.puts("Hello, Newman.")
def hello("Newman"), do: IO.puts("Hello, Jerry.")
def hello(name), do: IO.puts("Hello #{name}!")
hello(nil) # ..?
hello("George") # Hello George!
hello("Jerry") # Hello, Newman.
With just 4 lines of code we’ve handled a nil case, and satisfied at least one Seinfeld fan. But surely, life is more complex.
How could we make this robust enough to handle a real world scenario?
defmodule PartyGuest do
@sworn_enemies ["Newman"]
defguardp is_crowd(guests) when is_list(guests) and length(guests) > 10
defdelegate float_around, to: PartyHost
def hello(nil), do: IO.puts("..?")
def hello(guests) when is_crowd(guests), do: IO.inspect("👋")
def hello(guests) when is_list(guests), do: Enum.map(guests, &hello(&1))
def hello(guest) when in @sworn_enemies, do: IO.puts("Hello, ${guest}.")
def hello(guest), do: IO.puts("Hello #{guest}!")
end
Now, if we give hello/1
a list of guests, it will greet all of them nicely one by one. Unless there’s a lot of them… in which case a wave will do. Also, we can relax - it’s the PartyHost module’s responsibility to float_around
all night. We’ve even added an extensible list of enemies to handle should the situation arise. Only 10 lines of code to get a pretty accurate simulation of me at a party - go figure.
What’s going on here? #
-
@sworn_enemies
: This is a module attribute. We can use these to encapsulate some domain specific knowledge under a namespace for readability. -
defguard
: Here we’re defining a custom guard. Guards give us a way to augment pattern matching with more complex checks. You probably noticed I actually useddefguardp
: Besidesdefmodule
anddefdelegate
, all of Elixir’sdef
variations can be made private to a module by appending the letter ‘p’ to the keyword. -
defdelegate
: Thedefdelegate
keyword allows us to call on another module’s implementation of a given function. This can help prevent repetition in our code, as well as keep implementation details encapsulated within their appropriate domains. -
&hello(&1)
: This is an anonymous function. The ampersand denotes the function invocation itself, while&N
refers to the N-th parameter. We also could’ve used the longform syntax:fn (guest) -> hello(guest) end
.
Important Note: #
The order we declare our functions is important. If I was to define the hello(guest)
function clause above the rest, none of the others would ever be called. This is because in this variant guest
acts as a catch-all that will match on any data type, and Elixir functions are evaluated top-down. You can read more about how pattern matching in Elixir works here.
This kind of thing can be taken even further by things like pattern matching on structs, but we’ll call that out of scope for this article. Hopefully these examples have given you a pretty clear idea of how powerful and expressive Elixir’s function declarations can be.