A single function definition lets you define different implementations, depending on the type and contents of the arguments passed. (You cannot select based on the number of arguments—each clause in the function definition must have the same number of parameters.)
At its simplest, we can use pattern matching to select which clause to run. In the example that follows, we know the tuple returned by File.open has :ok as its first element if the file was opened, so we write a function that displays either the first line of a successfully opened file or a simple error message if the file could not be opened.
1: | iex> handle_open = fn |
2: | ...> {:ok, file} -> "Read data: #{IO.read(file, :line)}" |
3: | ...> {_, error} -> "Error: #{:file.format_error(error)}" |
4: | ...> end |
5: | #Function<12.17052888 in :erl_eval.expr/5> |
6: | iex> handle_open.(File.open("code/intro/hello.exs")) # this file exists |
7: | "Read data: IO.puts \"Hello, World!\"\n" |
8: | iex> handle_open.(File.open("nonexistent")) # this one doesn't |
9: | "Error: no such file or directory" |
Start by looking inside the function definition. On lines 2 and 3 we define two separate function bodies. Each takes a single tuple as a parameter. The first of them requires that the first term in the tuple is :ok. The second line uses the special variable _ (underscore) to match any other value for the first term.
Now look at line 6. We call our function, passing it the result of calling File.open on a file that exists. This means the function will receive the tuple {:ok,file}, and this matches the clause on line 2. The corresponding code calls IO.read to read the first line of this file.
We then call handle_open again, this time with the result of trying to open a file that does not exist. The tuple that is returned ({:error,:enoent}) is passed to our function, which looks for a matching clause. It fails on line 2 because the first term is not :ok, but it succeeds on the next line. The code in that clause formats the error as a nice string.
Note a couple of other things in this code. On line 3 we call :file.format_error. The :file part of this refers to the underlying Erlang File module, so we can call its format_error function. Contrast this with the call to File.open on line 6. Here the File part refers to Elixir’s built-in module. This is a good example of the underlying environment leaking through into Elixir code. It is good that you can access all the existing Erlang libraries—there are hundreds of years of effort in there just waiting for you to use. But it is also tricky because you have to differentiate between Erlang functions and Elixir functions when you call them.
And finally, this example shows off Elixir’s string interpolation. Inside a string, the contents of #{...} are evaluated and the result is substituted back in.