Reputation: 651
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
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
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:
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.
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:
Good luck!
Upvotes: 4
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:
{:ok, file}
if the file exists{: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