Elixir 1.3
Chapter 6
Modules and
Named Functions
Module and Named Function
defmodule Times do
def double(n) do
n * 2
end
end
We have a module named Times and it contains a single function double. Since this function has a
single param it's name is identified as double/1.
Compiling a module
$ iex times.exs
iex> Times.double 4
8
If you’re already in iex, you can use the c helper to compile your file without returning to the command line.
iex> c "times.exs"
[Times]
iex> Times.double(4)
8
iex> Times.double(123)
246
Function Failure
iex> Times.double("cat")
** (ArithmeticError) bad argument in arithmetic expression
times.exs:3: Times.double/1
An exception (ArithmeticError) gets raised,
and we see a stack backtrace.
The first line tells us what went wrong,
and the second line tells us where.
Notice, the name of our function: Times.double/1.
Named Functions
In Elixir a named function is identified by both its name and its number of parameters (its arity).
Our double function takes one parameter, so Elixir knows it as double/1.
If we had another version of double that took three parameters, it would be known as double/3.
These two functions are totally separate as far as
Elixir is concerned.
The Function’s Body Is a Block
The do…end form is just a lump of syntactic sugar—during compilation it is turned into the do: form.
Typically people use the do: syntax for single-line blocks, and do…end for multiline ones.
defmodule Times do
def double(n) do
n * 2
end
end
defmodule Times do
def double(n), do: n * 2
end
defmodule Times, do: (def double(n), do: n*2)
Function Calls and Pattern Matching
Named functions use pattern matching to bind their parameter list to the passed arguments just like anonymous functions but with named functions we can write the function multiple times, each with it's own parameter list and body.
defmodule Factorial do
def of(0), do: 1
def of(n), do: n * of(n-1)
end
What happens when we call Factorial.of(2)?
Function Calls and Pattern Matching
defmodule Factorial do
def of(0), do: 1
def of(n), do: n * of(n-1)
end
This pattern of design and coding is very common in Elixir (and almost all functional languages).
First look for the simplest possible case, one that has a definite answer. This will be the anchor.
Then look for a recursive solution that will end up calling the anchor case.
Function Calls and Pattern Matching
defmodule BadFactorial do
def of(n), do: n * of(n-1)
def of(0), do: 1
end
The order of these clauses can make a difference when you translate them into code. Elixir tries functions from the top down, executing the first match.
iex> c "factorial1-bad.exs"
.../factorial1-bad.ex:3: this clause cannot match because a previous clause at
line 2 always matches
Elixir helps out:
Guard Clauses using when
defmodule Guard do
def what_is(x) when is_number(x) do
IO.puts "#{x} is a number"
end
def what_is(x) when is_list(x) do
IO.puts "#{inspect(x)} is a list"
end
def what_is(x) when is_atom(x) do
IO.puts "#{x} is an atom"
end
end
Guard.what_is(99) # => 99 is a number
Guard.what_is(:cat) # => cat is an atom
Guard.what_is([1,2,3]) # => [1,2,3] is a list
In pattern matching a function,
what if we need to distinguish based on their
types or on some test involving their values?
Guard Clauses
# old example
defmodule Factorial do
def of(0), do: 1
def of(n), do: n * of(n-1)
end
# checking for positive numbers
defmodule Factorial do
def of(0), do: 1
def of(n) when n > 0 do
n * of(n-1)
end
end
iex> c "factorial2.exs"
[Factorial]
iex> Factorial.of -100
** (FunctionClauseError) no function clause matching in Factorial.of/1...
Guard-Clause Limitations
You can write only a subset of Elixir expressions in guard clauses. There are many operators and the following:
Type-check functions, return true/false
is_atom is_binary is_bitstring is_boolean is_exception is_float is_function is_integer is_list is_map is_number is_pid is_port is_record is_reference is_tuple
Return values
abs(number) bit_size(bitstring) byte_size(bitstring) div(number,number) elem(tuple, n) float(term) hd(list) length(list) node() node(pid|ref|port) rem(number,number) round(number) self() tl(list) trunc(number) tuple_size(tuple)
Default Parameters
- When calling a function with default parameters, Elixir compares the number of arguments you are passing with the number of required parameters for the function.
- If you’re passing fewer arguments than the number of required parameters, then there’s no match.
- If the two numbers are equal, then the required parameters take the values of the passed arguments, and the other parameters take their default values.
- If the count of passed arguments is greater than the number of required parameters, Elixir uses the excess to override the default values of some or all parameters.
- Parameters are matched left to right.
Default value looks like param \\ value
Default Parameters
The first function definition (with the default parameters) matches any call with two, three, or four arguments.
defmodule Example do
def func(p1, p2 \\ 2, p3 \\ 3, p4) do
IO.inspect [p1, p2, p3, p4]
end
end
Example.func("a", "b") # => ["a",2,3,"b"]
Example.func("a", "b", "c") # => ["a","b",3,"c"]
Example.func("a", "b", "c", "d") # => ["a","b","c","d"]
def func(p1, p2 \\ 2, p3 \\ 3, p4) do
IO.inspect [p1, p2, p3, p4]
end
def func(p1, p2) do
IO.inspect [p1, p2]
end
** (CompileError) default_params.exs:7: def func/2 conflicts with
defaults from def func/4”
Private Functions
The defp macro defines a private function—one that can be called only within the module that declares it.
defmodule Module do
def fun(a) when is_list(a), do: true
defp fun(a), do: false
end
The Amazing Pipe Operator: |>
people = DB.find_customers
orders = Orders.for_customers(people)
tax = sales_tax(orders, 2016)
filing = prepare_filing(tax)
# or
filing = prepare_filing(sales_tax(Orders.for_customers(DB.find_customers), 2013))
Elixir has a better way of writing it.
filing = DB.find_customers
|> Orders.for_customers
|> sales_tax(2016)
|> prepare_filing
The |> operator takes the result of the expression to its left and inserts it as the first parameter of the function invocation to its right.
So the list of customers the first call returns becomes the argument passed to the for_customers function.
The Amazing Pipe Operator: |>
iex> (1..10) |> Enum.map(&(&1*&1)) |> Enum.filter(&(&1 < 40))
[1, 4, 9, 16, 25, 36]
val |> f(a,b) is basically the same as calling f(val,a,b). You can also use chain the calls on the same line.
Note that use of parentheses in that code—the & shortcut and the pipe operator fight otherwise.
You should always use parentheses around function parameters in pipelines.
Programming is transforming data, and the |> operator makes that transformation explicit.
Modules
defmodule Mod do
def func1 do
IO.puts "in func1"
end
def func2 do
func1
IO.puts "in func2"
end
end
Mod.func1
Mod.func2
If we want to reference a function defined in a module from outside that module, we need to prefix the reference with the module’s name
Nested Modules
defmodule Outer do
defmodule Inner do
def inner_func do
end
end
def outer_func do
Inner.inner_func
end
end
Outer.outer_func
Outer.Inner.inner_func
Module nesting in Elixir is an illusion—all modules are defined at the top level. Elixir simply prepends the outer module name to the inner module name by putting a dot between the two.
Directives for Modules
Elixir has three directives that simplify
working with modules.
All three are executed as your program runs, and the effect of all three is lexically scoped—
it starts at the point the directive is encountered, and
stops at the end of the enclosing scope.
- import
- alias
- require
import Directive
The import directive brings a module’s functions and/or macros into the current scope.
The full syntax of import is
import Module [, only:|except: ]
defmodule Example do
def func1 do
List.flatten [1,[2,3],4]
end
def func2 do
import List, only: [flatten: 1]
flatten [5,[6,7],8]
end
end
You write only: or except:,
followed by a list of name: arity pairs.
alias Directive
The alias directive creates an alias for a module.
defmodule Example do
def compile_and_go(source) do
alias My.Other.Module.Parser, as: Parser
alias My.Other.Module.Runner, as: Runner
source
|> Parser.parse()
|> Runner.execute()
end
end
We could have abbreviated these alias directives to
alias My.Other.Module.Parser
alias My.Other.Module.Runner
because the as: parameters default to the last part of the module name. We could even take this further, and do:
alias My.Other.Module.{Parser, Runner}
require Directive
You require a module if you want to use any macros it defines.
This ensures that the macro definitions are available when your code is compiled.
We’ll talk about require when we talk about macros.
Module Attributes
Elixir modules each have associated metadata. Each item of metadata is called an attribute of the module and is identified by a name.
You can access these attributes by prefixing the name with an at sign (@).
This works only at the top level of a module—you can’t set an attribute inside a function definition.
You can, however, access attributes inside functions.
The format is:
@name value
Module Attributes
This works only at the top level of a module—you can’t set an attribute inside a function definition.
You can access attributes inside functions.
defmodule Example do
@author "Dave Thomas"
def get_author do
@author
end
end
IO.puts "Example was written by #{Example.get_author}"
Module Attributes
You can set the same attribute multiple times in a module. If you access that attribute in a named function in that module, the value you see will be the value in effect when the function is defined.
defmodule Example do
@attr "one"
def first, do: @attr
@attr "two"
def second, do: @attr
end
IO.puts "#{Example.first} #{Example.second}" # => one two”
Use them for configuration and metadata only. They are like a constant in other languages.
Module Names:
Elixir, Erlang, and Atoms
Internally, module names are just atoms. When you write a name starting with an uppercase letter, such as IO, Elixir converts it internally into an atom called Elixir.IO.
iex> is_atom IO
true
iex> to_string IO
"Elixir.IO"
iex> :"Elixir.IO" === IO
true
So a call to a function in a module is really an atom followed by a dot followed by the function name
iex> IO.puts 123
123
iex> :"Elixir.IO".puts 123
123”
Calling a Function in an Erlang Library
The Erlang conventions for names are different—variables start with an uppercase letter and atoms are simple lowercase names.
The Erlang module timer is called just that, the atom timer.
In Elixir we write that as :timer.
If you want to refer to the tc function in timer, you’d write :timer.tc. (Note the colon at the start.)
iex> :io.format("The number is ~3.1f~n", [5.678])
The number is 5.7
:ok
Thank you!
Programming Elixir 1.3 Chapter 06
By Dustin McCraw
Programming Elixir 1.3 Chapter 06
- 1,177