Elixir 1.6 has been released with a lot of nice additions to the language, but without a doubt, what I’m most pumped about is real guard
support in the language.
Before 1.6, you could constraint a function like so:
def change_settings(%User{admin: is_admin}, settings) when is_admin == true do
# Update the settings is only available for admins
end
This worked just fine, but now with real guard
support, we can extract those checks to make reuse and composition easier!
defmodule User do
defstruct [:karma, :friend_count]
end
defmodule Main do
defmacro can_update_settings(karma, friend_count) do
quote do
unquote(karma)> 100 and unquote(friend_count) > 3
end
end
def change_settings(user = %User{karma: karma, friend_count: count}, settings) when can_update_settings(karma, count) do
IO.puts "Updating settings..."
{:ok, user}
end
def add_member(user = %User{karma: karma, friend_count: count}, new_user) when can_update_settings(karma, count) do
IO.puts "Adding member"
{:ok, user}
end
def main do
%User{karma: 101, friend_count: 4}
|> change_settings(%{})
end
end
Main.main
// => "Updating settings..."
You can skip the manual macro creation alltogether if you use defguard
and defguardp
, though. These are new functions in the Kernel module that just take your constraint and generate a macro suitable for use as a guard from it.
defguard can_update_settings(verified) when verified == true
I specially love the fact that now you can also use multiple guards with functions:
defmodule User do
defstruct [:karma, :friend_count]
end
defmodule CommunityFactory do
defguard has_enough_karma(karma) when karma > 100
defguard has_invited_enough_friends(friend_count) when friend_count > 3
def create_community(%User{karma: karma, friend_count: count}, com_name)
when has_enough_karma(karma)
when has_invited_enough_friends(count) do
IO.puts "Creating community..."
{:ok, %{name: com_name}}
end
def create_community(_user, com_name) do
IO.puts "Cannot create #{com_name}. User still does not complete onboarding."
{:error, nil}
end
end
defmodule Main do
def main do
%User{karma: 101, friend_count: 5}
|> CommunityFactory.create_community("My Community")
end
end
Main.main
// => Creating community...
I use guards like crazy in Swift. I think they’re a great tool to structure your code. However, in Swift, guards can be used (and maybe abused) more as a fancy if/else
dance, rather than a mean to express condition conformance.
For instance, in Swift, the only requirement for a guard
is that it breaks the flow of the function:
guard user.isAdmin else {
print("User is not admin!") # Compiling error, does not break function flow
}
// Update settings here
But literally nothing stops you from transferring the control over to another function!
guard user.isAdmin else {
showErrorMessage(.notAdmin) // Give up control of the execution flow
return
}
// Update settings here
Although this may seem harmless, it can cost some precious down the road when you’re trying to debug something that just follows a trail of guarded statements that keep on going forever.
With Elixir’s flavor of guards
, this is explicitly not possible:
Not all expressions are allowed in guard clauses, but only a handful of them. This is a deliberate choice: only a predefined set of side-effect-free functions are allowed. This way, Elixir (and Erlang) can make sure that nothing bad happens while executing guards and no mutations happen anywhere.
Altough I love Swift, I do think that its guard
does not entirely guard you from shooting yourself in the foot.
Small but thoughtful updates like this to the language really make me hopeful for the future of Elixir.
You can read everything about the new Elixir guard
s on the official documentation page and read the release announcement here.