Function overloading in elixir

The syntax below is throwing when I pass a List as input. Its always expecting a String.

 @spec do_things(String.t()) :: String.t()
  def do_things(input) do
    String.split(input, "\n")
    |> |Enum.map(&(String.graphemes(&1)))
    |> do_things()
  end

  @spec do_things([List]) :: String.t()
  def do_things(list) do
     #do things and return string
  end
1 Like

Since the compiler will see these as the same function definition you will need to help it by defining under what circumstances each of these should be executed. In Elixir, a guard clause does this. So in your case, for example:

  @spec do_things(String.t() | list()) :: String.t()
  def do_things(input) when is_binary(input) do
    String.split(input, "\n")
    |> do_things()
  end

  def do_things(list) when is_list(list) do
     #do things and return string
  end

Note that there can only be one @spec since a function is defined by its name and arity and therefore these are both the same function.

7 Likes

Amazing Community! Thank you very much Kip!

a follow up question

What will be spec syntax if I want to return different data types;
for example, one method returns String and another returns a list and another return in an integer.

Like this:

@spec do_things(String.t() | list()) :: String.t() | list() | integer

Understandably a function that has variable return types will be more difficult to compose and probably harder to debug. The case of returning a valid result or an error return wrapped in a tuple like {:error, reason} is idiomatic however.

3 Likes

makes sense. I ended up in syntax like this:

@spec do_things(String.t()) :: String.t()
def do_things(input) when is_binary(input) do
  #do things with string and return string
end

@spec do_things(list()) :: integer
def do_things(list) when is_list(list) do
  #do things with list and return integer
end

and @kip, I agree with you, same functions with different return types are difficult to debug. I am pretty new to elixir and just trying out different things for mastering the language

I learnt something: I thought only one @spec was allowed but clearly not. A h Thing.do_things in iex returns:

  @spec do_things(String.t()) :: String.t()
  @spec do_things(list()) :: integer()

Thats a good thing!

5 Likes

Considering that you’re defining the spec for the same function, that may have different return types, you could define the return type as an union of String.t() | list(integer) | integer.

Just like @kip defined the argument as String.t() | list()

Types like number() are an union of integer() | float() as you can see in https://hexdocs.pm/elixir/typespecs.html

1 Like

In terms of intent

  @spec do_things(String.t()) :: String.t()
  @spec do_things(list(integer())) :: integer()

would communicate to me that I’ll get String.t() if I provide a String.t() and that I’ll get an integer if I provide a list(integer()) which

  @spec do_things(String.t() | list()) :: String.t() | integer()

does not.

I doubt that it would make an iota of difference to dialyzer though.

3 Likes

Sure, that’s much better. Forgot about it. :+1:

Although, if a function has many different return types it’s a sign that function may be doing too much things, or using a string or integer to indicate error (like an error code or error message) when it could benefit from an ok/error tuple (but that’s another subject).

1 Like