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.







