3 ms·
Having dabbled a bit in Elixir during the last year, I've been wondering why function "overloads" are a thing in Elixir, since they (as the author points out) s
by manmal 3y ago
Having dabbled a bit in Elixir during the last year, I've been wondering why function "overloads" are a thing in Elixir, since they (as the author points out) seem to be equivalent to `case` expressions. Do polymorphic functions provide any tangible benefits?
- OkayPhysicist 3y agoThe big advantage is that it saves two levels of indentation and a lot of repeated pattern matching. def equal?(x,y) when x == y do true end def equal?(_,_), do: false would be equivalent to def equal?(x,y) do case {x,y} do {x,y} when x == y -> true _ -> false end end It's also handy in places like GenServers, where from a reading perspective clustering the hooks and their helper functions together is far more readable than the alternative. def handle_call(:msg1,_from,_state) do helper1() helper2() etc... end def helper1(), do: 1 def helper2(), do: 2 def handle_call(:msg2,_from,_state), do: :err Is a lot better than def handle_call(msg,_from,_state) do case {msg, _from, _state} do {:msg1,_from,_state} -> helper1() helper2() etc... {:msg2,_from,_state} -> :err end end def helper1(), do: 1 def helper2(), do: 2 Especially remembering that you'll likely have a lot more than 2 message callbacks.
- ch4s3 3y agoI wouldn't really call them overloads, but you're right they are a lot like case statements. The benefit is that you can combine them with pattern matching to vastly simplify how you handle edge cases. You can pull nil and error handling out of your main logic and into function heads. You can easily separate logic for different input types and values. You can do all sorts of cool stuff with it.
- lawik 3y agoI believe they literally compile down to case statements.
- ch4s3 3y agoHi Lars! Of course you're right[1], but function heads can handle more complicated patterns than case statements, and compile down to a more complicated set of cases. So you can think of function heads/guards as an abstraction over case statements (if you squint a little). [1] https://learnyousomeerlang.com/syntax-in-functions https://learnyousomeerlang.com/syntax-in-functions
- OkayPhysicist 3y agoTo my knowledge, that's not true (at least for Elixir), and that's not what your source says, either. So long as you wrap your case arguments into a tuple, you can have the same level of complexity in either.
- phanimahesh 3y agoThey are equally powerful, however the multi head approach is more convenient. You can for example have a macro inject a bunch of functions, and override just one specific head, which becomes unwieldy and significantly less maintainable with just case.
- giraffe_lady 3y agoThe unsatisfying answer is that elixir inherits this from erlang. I don't have a good answer on why erlang does it though. For functions of different arities, erlang (and therefor elixir) considers them completely different functions, and requires a declaration for each arity at least. But then elixir adds optional args which confuses the answer a little. Being able to hard code handling of exceptional constants in a separate body is very useful but like you said it's conceptually the same thing as a pattern match, and not really an answer about why that feature is there. I've looked a bit before but I haven't come up with anything more compelling than "erlang was kinda off doing its own thing, syntax-wise, and so some of it is just Like That now."
- sodapopcan 3y agoElixir doesn't technically have default arguments, and this isn't such a pedantic distinction since what it has is a macro that defines a whole other function. defmodule Foo do def bar(baz \\ "baz") do baz end end becomes defmodule Foo do def bar do "baz" end def bar(baz) do baz end end Which, as you pointed out, are two completely separate functions.
- giraffe_lady 3y agoAhh that makes so much sense, I feel like I should have guessed something like that was going on. Thanks!
- Jtsummers 3y ago> "erlang was kinda off doing its own thing, syntax-wise, and so some of it is just Like That now." The syntax makes a lot more sense if you go into the history (well-documented) and understand that Erlang's initial implementation was in Prolog and a lot of the syntax can be seen as a consequence of that. As for pattern matching in the function signature instead of forcing dropping to a case expression, it removes a level of useless and noisy indentation and, again, is a lot more like Prolog. Even the use of , and ; as separators in Erlang make sense given that heritage. In Prolog a sequence like this: A, B, C. Can be read as "A and B and C" while: A; B; C. Can be read as "A or B or C". So function bodies using , as a separator and ; between alternatives with . as a terminal follows straight from Prolog.
- notpublic 3y agoI find it quite readable especially with nested logic. Here is an example: https://github.com/elixir-ecto/ecto/blob/master/lib/ecto/query/builder.ex#L79 https://github.com/elixir-ecto/ecto/blob/master/lib/ecto/que... Edit: Just remembered another use case where function overload is used to override default implementation defmodule Downloader do use GenServer def handle_cast(:start, state) do ... end def handle_cast(:stop, state) do ... end end Here, "use GenServer" imports default implementation of various handlers which can be overridden. https://github.com/elixir-lang/elixir/blob/main/lib/elixir/lib/gen_server.ex#L857C14-L929 https://github.com/elixir-lang/elixir/blob/main/lib/elixir/l...
- lkuty 3y agoIt is the same function with different clauses using pattern matching. It is a powerful way of handling different cases. There is no overload at all. The concept does not exist in the language. And if the arity differs, then they are completely different functions even if the name is the same.
- sodapopcan 3y agoThey are useful for recursion: def factorial(0), do: 1 def factorial(n) when n > 0, do: n * factorial(n - 1) Otherwise it comes down to a matter of taste. Sometimes they read nicer than a `case`, sometimes a `case` is better. Like I would favour: def zero?(0), do: true def zero?(_), do: false over the case version, for example, but that's just me.
- sodapopcan 3y ago...and, just to be "that guy" to myself before anyone else does over my contrived example: "Or just:" def zero?(n), do: n == 0
- mikhailfranco 3y agoThere's only one situaton where I prefer a single function with a nested case: when there is a datatype guard check for an argument that applies to all invocations (precondition). This belongs on the function, then the case is pure discrimination within that type. If multiple function heads were used, you might have to repeat the typecheck guard every time, which is ugly in code, but probably not inefficient (the Erlang pattern/guard compiler is insanely good).
- lkuty 3y agoIn Phoenix core_components.ex, you have an interesting way of dealing with common code and also keeping the valuable pattern matching with functions. This way of handling things may not be applicable to every situation. def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do assigns |> assign(field: nil, id: assigns.id || field.id) # <-- ALLOWS TO AVOID RE-ENTERING THIS FUNCTION CLAUSE |> assign(:errors, Enum.map(field.errors, &translate_error(&1))) |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end) |> assign_new(:value, fn -> field.value end) |> input() # <-- RECURSE WHICH ALLOWS TO CHOOSE THE RIGHT FUNCTION CLAUSE BELOW end def input(%{type: "checkbox", value: value} = assigns) do ... end def input(%{type: "select"} = assigns) do ... end ...
- peoplefromibiza 3y agothey both compile to a case expression in Abstract Erlang Format another use case that has not been mentioned is generated functions you can't generate case statements as easily