M.Nar
M.Nar

Reputation: 651

Understanding Elixir functions with multiple clauses

I recently started learning Elixir. Coming from an object oriented programming background I am having trouble understanding Elixir functions.

I am following Dave Thomas's book Programming Elixir >= 1.6, but I do not quite understand how functions work.

In the book, he has the following example:

handle_open = fn
  {:ok, file} -> "Read data: #{IO.read(file, :line)}"
  {_,  error} -> "Error: #{:file.format_error(error)}"
end

handle_open.(File.open(​"​​code/intro/hello.exs"​))   ​# this file exists​
-> "Read data: IO.puts \"Hello, World!\"\n"

 handle_open.(File.open(​"​​nonexistent"​))           ​# this one doesn't​
 -> Error: no such file or directory"

I do not understand how the parameters work. Is there an implicit if, else statement hidden somewhere?

Upvotes: 2

Views: 4505

Answers (3)

rld
rld

Reputation: 2763

A good explanation for work with files is this book: joyElixir. It's a good place for start and is a small book.

Upvotes: 0

7stud
7stud

Reputation: 48599

In elixir, you can define functions with multiple clauses and elixir employs what's called pattern matching to determine which clause to execute. As you suspected, when you define multiple function clauses you effectively create an implicit if-else if, e.g. if the function arguments specified in the function call match the first function clause's parameters, then execute the first function clause, else if the function arguments specified in the function call match the second function clause's parameters, then execute the second function clause, etc.. Here is an example:

my.exs:

defmodule My do

  def calc(x, 1) do
    x * 3
  end
  def calc(x, 2) do
    x - 4
  end

  def test do
    IO.puts calc(5, 1)
    IO.puts calc(5, 2)
  end

end

(I typically do not leave a blank line between the multiple clauses of a function definition to indicate that they are all the same function.)

In iex:

$ iex my.exs
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (1.6.6) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> My.test
15
1
:ok

iex(2)>

When you call calc(5, 1) elixir goes to the definition of calc(), and elixir starts with the first function clause and tries to match the args 5, 1 against the parameters x, 1. Because the parameter x is a variable, it can match anything, while the parameter 1 can only match 1. Therefore, there is a match and x gets assigned the value 5. In other languages, you can't have values, like 1, "hello",%{a: 1} or :error, specified as a function parameter in a function definition--rather all the function parameters have to be variables.

Similarly, when you call calc(5, 2), elixir goes to the definition of calc(), and elixir starts with the first function clause and tries to match the arguments 5, 2 in the function call against the parameters x, 1. The x parameter can match anything but the 1 parameter does not match the 2 argument, so elixir goes to the next function clause and tries to match the arguments 5, 2 against the parameters x, 2. That produces a match and x gets assigned 5.

Elixir has some pretty interesting rules for matching. Here are a couple of rules that you will come across at some point, which can be hard to decipher:

  1. def func(%{] = x) will match any argument that is a map--not just an empty map--and the map will get assigned to the variable x. The same goes for structs, e.g. def func(%Dog{} = x) will match any Dog struct.

  2. def func("hello " <> subject) will match any string argument that starts with "hello ", and the rest of the string will be assigned to the subject variable.

Elixir also allows you to define anonymous functions with multiple clauses. If you change test() to:

  def test do
    func = fn 
              (:ok, x)    -> IO.puts ":ok branch, x = #{x}"
              (y, :error) -> IO.puts ":error branch, y = #{y}"
           end

    func.("hello", :error)
    func.(:ok, 10)
  end

then in iex you will see:

~/elixir_programs$ iex my.exs
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (1.6.6) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> My.test()  
:error branch, y = hello
:ok branch, x = 10
:ok

iex(2)> 

The matching works just like with named functions: elixir looks at the function clauses in order and tries to match the arguments in the function call against the parameters in the function clauses. Note that the order in which you define the function clauses in your code can matter. It's easy to define a function clause that will never execute, for instance:

func = fn 
          (x)    ->  ...
          (:ok)  ->  ...
       end

Because the parameter x in the first function clause will match any argument, the second function clause can never execute. Luckily, elixir will warn you if you do that.

And, because elixir is a functional language it would be remiss to not show a recursion example. Add the following definition of count() to the My module:

  def count(0) do
    :ok
  end
  def count(n) when n > 0 do
    Process.sleep 1_000  #sleep for 1 second
    IO.puts n
    count(n-1)
  end

Then in iex:

 ~/elixir_programs$ iex my.exs 
 Erlang/OTP 20 [erts-9.3] [source]
 [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe]
 [kernel-poll:false] Interactive Elixir (1.6.6) - press Ctrl+C to exit
 (type h() ENTER for help)

iex(1)> My.count(10)
10
9
8
7
6
5
4
3
2
1
:ok

iex(2)>

When the code calls count(n-1), elixir goes to the first function clause of count(), then tries to match whatever the value of the argument n-1 is against the parameter 0 in the function clause. If there's no match, elixir tries the second function clause. As a result, the second function clause keeps matching the function call count(n-1) until all the numbers including 1 are output, whereupon n-1 is equal to 0 in the function call, resulting in the function call count(0), which finally matches the first function clause, and the first function clause doesn't output anything nor does it call itself, so the recursion ends.

The key things to learn in elixir are:

  1. Pattern matching
  2. Recursion
  3. Spawning other processes

Good luck!

Upvotes: 4

Sheharyar
Sheharyar

Reputation: 75740

There are a couple of things going on here, and I'll try to cover all of them. For starters, there are two different functions being used here. One is the named function (File.open) and the other one is an anonymous function you created, assigned to the variable handle_open. There's a slight difference in the way both are called.

When you call the File.open function inside the handle_open function it, basically means you are calling handle_open on its result. But the File.open/2 function itself can return two values:

  1. {:ok, file} if the file exists
  2. {:error, reason} if it doesn't (or if there's another error)

The handle_open function uses pattern matching and multiple function clauses to check what the response was and returns the appropriate message. If the given value "matches a specified pattern" it executes that statement, otherwise it checks against the next pattern. Though in a sense, it is similar to an if-else statement, a better analogy is the case keyword:

result = File.open("/some/path")

case result do
  {:ok, file} ->
    "The file exists"

  {:error, reason} ->
    "There was an error"
end

Upvotes: 5

Related Questions