This is not function overloading at all. You can liken it to a multimethod maybe.
Using pattern matching in function heads does get compiled to case. It’s a readability improvement pattern that many have found useful because you can more cleanly isolate logic for separate input values.
There’s never an universal answer. That depends on whether you are reaping any readability benefits and/or if your team is happier with the code.
It’s the best pattern I came across and I love it.
Here are some of the benefits:
No pyramid of doom.
Each aspect belongs to its own function.
Functions can be pipelined because of this approach and we can easily debug the things happening in a pipeline using dbg(), rather than putting print statements everywhere.
Pattern Matching…
Here’s an example code I came across today, that is just awesome:
Here’s how I use it:
defmodule DerpyCoder.Photos.Policy do
@moduledoc """
Policy: Used to authorize user access
"""
alias DerpyCoder.Accounts.User
alias DerpyCoder.Photos.Photo
@type entity :: struct()
@type action :: :new | :index | :edit | :show | :delete
@spec can?(User, action(), entity()) :: boolean()
def can?(user, action, entity)
# ==============================================================================
# Super Admin - Can do anything
# ==============================================================================
def can?(%User{id: id, role: :super_admin}, _, _), do: DerpyCoder.Accounts.is_super_admin?(id)
# ==============================================================================
# Admin
# ==============================================================================
def can?(%User{role: :admin} = user, :new, _),
do: FunWithFlags.enabled?(:new_photos, for: user) # Admins must have the feature enabled
def can?(%User{role: :admin} = user, :edit, _),
do: FunWithFlags.enabled?(:edit_photos, for: user) # By being part of photography group perhaps
def can?(%User{role: :admin} = user, _, Photo), do: FunWithFlags.Group.in?(user, :photography)
def can?(%User{role: :admin}, _, _), do: true
# ==============================================================================
# User
# ==============================================================================
def can?(%User{} = user, :new, Photo), do: FunWithFlags.enabled?(:new_photos, for: user)
def can?(%User{id: id} = user, :edit, %Photo{user_id: id}), # Can user edit photo? Yes, only if it's theirs
do: FunWithFlags.enabled?(:edit_photos, for: user) # And if feature is enabled
def can?(%User{id: id} = user, :delete, %Photo{user_id: id}), # Can user delete photo? Yes, only if it's their own
do: FunWithFlags.enabled?(:delete_photos, for: user) # And if the feature is enabled for the user
def can?(_, _, _), do: false
end
defmodule DerpyCoder.Photos do
@moduledoc """
The Photos context.
"""
defdelegate can?(user, action, entity), to: DerpyCoder.Photos.Policy
...
end
Seeing the above, can you tell what’s happening?
Is it more readable, perhaps not, but once you get used to it, it’s much more readable than an if-else-infested page.
And here’s the elegant usage of the above policy.
Photos.can?(@current_user, :new, Photo) # Show new button
Photos.can?(@current_user, :delete, photo) # Show delete button
Photos.can?(@current_user, :edit, photo) # Show edit button
It’s because of this feature, handle_event, handle_info, plugs, and pipelines work the way they work.
I’ll expand on this a little bit. One of my favorite parts of Elixir and functional programming is the ability to “say what you want - not what you don’t”
In other words, you write much less “defensive” code in Elixir. A typical imperative function with some super simple validation might look like:
if (!form.IsValid)
{ return; }
// now okay, continue with business logic
Elixir
def submit({is_valid: true}) do
// now we know we’re good to go
end
def submit(form) do
// form is invalid, log, return error, etc;
end
The difference in this contrived example is admittedly small. However the principle is important. Each function head allows us to deal with exactly what it needs to be: we know that the first function head has a valid form and we don’t need to waste mental processing time worrying about what could go wrong. Likewise, in the second, we know the form isn’t valid and we can log / return an error / do whatever to tell the client the request needs to be retried
Exactly, and the great thing is that these principles can be applied to other languages too, in some easier and in some you might require some libraries.
Golang is one of the languages that also embraces this principle, errors as data, handle the errors you want and ignore the rest, the only sad thing about this in golang is the lack of support structure for this style of programming, people tend to write coroutines that can handle/ignore almost all errors as to avoid crashing the entire application, witch in turn creates defensive programming, that is brittle, hard to test, hard to debug.
In my short professional career with golang, I never saw anyone not writing defensive code, thousands of lines for null pointers check, this was one of the reasons at that point I decided to leave that company and golang behind and focus on elixir.
Which is kind of strange because Elixir has nil as well.
But I do get your point. Every time I write a non-trivial Golang program, a good chunk of the code is defensive programming. There has to be a better way for it, hope they are working on it. They recently made it possible to follow entire chains of errors – and you can link them manually – but it just makes error-checking code even bigger.
To the untrained Elixir eye, I wasn’t sure if this was a good example and use of Elixir’s pattern matching or if this was an example where perhaps it had been taken a bit too far. I can appreciate the power of pattern matching in Elixir, I just found it surprising to see it used when the pattern is so trivial, hence the inquiry.
In other languages, for a fixed set of possible values, I might just index into an Array or switch over a given value. Given that Elixir supports both strategies, I was curious if the implementation above offered specific advantages I wasn’t aware of.
(And if the answer is simply “This is how you do things in Elixir because Elixir isn’t like other languages.”, then that works for me.)
I think it’s a really fair question and in this case, I’d say either is okay. I personally think the way Plug has done it is very readable – while there’s a bit of duplication, it’s easy to skim past it and see where the match is occurring.
I don’t think this matters too much, but it also saves a few lines over case:
defp weekday_name(1), do: "Mon"
defp weekday_name(2), do: "Tue"
defp weekday_name(3), do: "Wed"
defp weekday_name(4), do: "Thu"
... snip ...
# vs
defp weekday_name(day_of_week) do
case day_of_week do
1 -> "Mon"
2 -> "Tue"
3 -> "Wed"
4 -> "Thu"
... snip ...
end
end
Edited to add:
As for other ways of defining the mapping – I think that makes the most sense when you have more metadata and it’s nice to keep it all together in one place. For instance:
I have a feeling that the errors in golang were designed by mistake, or at the very least the community never understood them, because every time I return to golang and look at it from the point of view of modern language design, it is just abysmal, templating engine is horrible, peculiar way to organize functions that is like extension functions, lack of code generation tools in the language leading to some libraries that want to abstract doing some really shady things under the hood. I am lucky that this is elixirforum and not golang forum , but I think that language should be thrown into the garbage and replaced with a better language.