Elixir Function Overloading
I like callsites to decide what they want, call a function, and get the expected value. That’s how I normally code. When I see logic inside a function alter what it will do, that seems dishonest to me.
Honest Code
function add100(x) {
return x + 100
}
add100(1) // 101
add100('potato') // 'potato100'
That is a dependable function. It does exactly what it should. The problem here is the callsite called the wrong function. People will usually try and fix this by shifting responsibility from the caller to the function (as if it can anticipate every stupid thing a callsite may do).
Dishonest Code
function add100(x) {
if (typeof x === 'number') {
return x + 100
}
}
add100(1) // 101
add100('potato') // undefined
Now this prevents the 'potato100'
bug, but a new undefined
bug is fast on
its heels. Why in the world is it the function’s problem if you use it
incorrectly?
Suspending Disbelief
Now that I’ve gotten that off my chest, let’s talk about how we solve this problem in Elixir. The examples I run across encourage you to use function overloading (different arity), multi-clause functions (same arity), or guards to move the decision making from the callsite down to the function.
defmodule Foo do
def add100(x) when is_integer(x) do
x + 100
end
end
Foo.add100(1) # 101
Foo.add100('potato') # (FunctionClauseError) no function clause matching in Foo.add100/1
This feels really weird to me, but I’m comforted by the error. It’s an
improvement over the undefined
bug our JS implementation created.
Acceptance
Part of learning a new language is being open to new ways of thinking. As such, it seems that the Elixir way is to ensure a function knows how to handle all the different patterns that may be thrown at it. If a new pattern is used, it raises an error and you are forced to expand the repertoire (or fix the callsite). That seems reasonable.
The way I’ve done it in the past suggests that, if the callsite has a new way to use a function, it would need to call a different function. It would technically be a different function, but essentially the same. The onus is then placed on the caller to understand the variations of a function and choose between them.
Put Down that Koolaide
Before I get all weak-kneed for Elixir and its function overloading, there’s just one more thing I can’t come to terms with.
defmodule Foo do
def bar(x) when x < 100 do
x + 100
end
def bar(x) do
x + 100000
end
def bar do
["a", "b", "c"]
end
end
Foo.bar(1) # 101
Foo.bar(100) # 100100
Foo.bar() # ["a", "b", "c"]
The variations of a function do not need to return the same data structure!
That doesn’t sit right with me. If I’m calling bar
and then playing with the
result, what the hell happens when someone changes a guard for bar and suddenly
I start getting back a different type of data?
Is this a real concern? Would this ever happen in real life? Will the compiler guide me right to it? Would it be easier to add the variation to all the callsites out in codeland?
I’m not sure right now. It feels weird to me, but I’ll go along for now and hopefully it will make more sense soon.
Refs
https://stackoverflow.com/questions/28377135/how-do-you-check-for-the-type-of-variable-in-elixir https://learnyousomeerlang.com/types-or-lack-thereof