#21: Function Overloading with Pattern Matching

·

4 min read

In Elixir, function overloading can be achieved through both pattern matching and defining functions with different numbers of arguments (arities).

In this post, we are going to take a closer look at the function overloading via pattern matching. Please see post #6 for an explanation of function overloading based on functions with different arities.

Function overloading can be achieved through pattern matching, where multiple function clauses with different patterns can exist within the same function definition. When a function is called, Elixir matches the arguments against the patterns in the function clauses and executes the body of the first matching clause.

Key points about function overloading with pattern matching in Elixir:

  1. Multiple Clauses: A function can have multiple clauses with different patterns.

  2. Pattern Matching: Elixir matches the function arguments against the patterns in the clauses to determine which clause to execute.

  3. Order Matters: Function clauses are evaluated from top to bottom, so the order of clauses is important.

  4. Flexible Behavior: Function overloading allows for different behavior based on the input arguments.

Example of function overloading with pattern matching in Elixir:

defmodule Math do
  def add(0, b), do: b
  def add(a, 0), do: a
  def add(a, b), do: a + b
end

IO.puts(Math.add(2, 3))  # Output: 5
IO.puts(Math.add(0, 5))  # Output: 5
IO.puts(Math.add(7, 0))  # Output: 7

In the above example, the add/2 function is overloaded with three clauses. The first clause handles the case where the first argument is 0, the second clause handles the case where the second argument is 0, and the third clause handles the general case of adding two numbers.

Additional Examples:

  1. Pattern Matching via the use of Guards:
defmodule Math do
  def add(a, b) when is_integer(a) and is_integer(b), do: a + b
  def add(a, b) when is_float(a) and is_float(b), do: a + b
end

IO.puts Math.add(2, 3)  # Output: 5 (integer addition)
IO.puts Math.add(2.5, 3.5)  # Output: 6.0 (float addition)

defmodule StringOps do
  def concat(a, b) when is_binary(a) and is_binary(b), do: a <> b
  def concat(a, b) when is_list(a) and is_list(b), do: List.flatten([a, b])
end

IO.puts StringOps.concat("Hello, ", "world!")  # Output: "Hello, world!"
IO.puts StringOps.concat(['H', 'e', 'l', 'l', 'o'], [' ', 'w', 'o', 'r', 'l', 'd', '!'])  # Output: "Hello world!"

defmodule Shape do
  def area({:rectangle, width, height}), do: width * height
  def area({:circle, radius}), do: 3.14 * radius * radius
end

Function overloading in Elixir is not a mysterious concept—it relies on familiar principles you already understand! Once you grasp its workings, you'll appreciate why functions are processed in a top-to-bottom manner.

When you define a function with multiple clauses, Elixir consolidates them into a single function represented by a case statement. For instance, a basic is_weekday/1 function can be structured as follows:

defmodule Examples.Patterns.Weekday do
  @weekdays ~w(Monday Tuesday Wednesday Thursday Friday)

  def is_weekday(day) when day in @weekdays do
    true
  end

  def is_weekday(_), do: false
end

IO.inspect Examples.Patterns.Weekday.is_weekday("Monday")  # Output: true
IO.inspect Examples.Patterns.Weekday.is_weekday("Saturday")  # Output: false

In this example, the Examples.Patterns.Weekday module defines a function is_weekday/1 that checks if a given day is a weekday based on a predefined list of weekdays. The function pattern matches the input day against the list of weekdays and returns true if it is a weekday, and false otherwise.

Here is the version of the Examples.Patterns.Weekday module with a single is_weekday/1 function represented by a case statement to determine if a given day is a weekday:

defmodule Examples.Patterns.Weekday do
  @weekdays ~w(Monday Tuesday Wednesday Thursday Friday)

  def is_weekday(day) do
    case day do
      day when day in @weekdays -> true
      _ -> false
    end
  end
end

IO.inspect Examples.Patterns.Weekday.is_weekday("Monday")  # Output: true
IO.inspect Examples.Patterns.Weekday.is_weekday("Saturday")  # Output: false

The Elixir compiler seamlessly handles this process, highlighting the importance of the order of function definitions from top to bottom. This feature also showcases the effective utilization of guards within case statements, as Elixir seamlessly converts them into case statements during compilation.

Function overloading with pattern matching in Elixir allows for concise and expressive code by defining different behaviors for different input patterns within the same function. This approach simplifies code maintenance and readability by encapsulating related logic in a single function definition.