kccarter
When should you use function overloading vs control structures?
While browsing through the source code to Plug, I can across this block of code:
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 ...
This pattern would raise a lot of red flags in most languages but given the popularity of Plug, I assume this is a reasonable pattern in Elixir.
Is this pattern preferred over using more “traditional” control structures like cond and case? If so, why and what benefits does it provide?
In Plug, there is a function for each weekday and each month. How far should you take such a design pattern when matching on simple values?
Most Liked
dimitarvp
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.
derpycoder
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.
D4no0
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.
zachallaun
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:
@days_of_week [
mon: [short_name: "Mon", long_name: "Monday"],
tue: [short_name: "Tue", long_name: "Tuesday"],
...
]
and then you might munge that data structure to create various lookup functions by atom, or index, or whatever.
JohnnyCurran
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







