Elixir
Table of Contents
1 Syntax
1.1 Match Operator (Pattern Matching)
In Elixir,
=
is a match operator: it looks for a way to make the left side the same as on the right.a = 1 # a becomes 1 1 = a # gives back 1 2 = a # error: cannot change value of 2, only value on LHS can be changed list = [1, 2, 3] [a, b, c] = [1, 2, 3] # matches a to 1, b : 2, c : 3
- A literal value in the pattern matches the exact value
A variable in the pattern matches by taking on the corresponding value
list = [1, 2, [1, 2, 3]] [a, b, c] = list a # 1 b # 2 c # [1, 2, 3]
1.1.1 Don't Care _
_
will ignore a value during the match.Like a wildcard saying "I'll accept any value here".
[1, _, _] = [1, 2, 3] # [1, 2, 3] [1, _, _] = [1, "c", "d"] # [1, "c", "d"]
[1, _, _]
matches any list of length three with first element = 1.
1.1.2 Pin Operator ^
to force Elixir to use existing value of the variable in the pattern, prefix the variable with
^
.a = 1 [^a, 2, 3] = [1, 2, 3] # gives [1, 2, 3] [^a, 2, 3] = [4, 2, 3] # give error, a cannot be matched to anything other than 1
1.1.3 v/s Erlang
- Elixir's pattern matching works same as Erlang, except in Erlang, a variable
once matched cannot be matched again.
- Erlang's meaning of
=
is similar to algebraic=
, when we sayx = a + 2
, we do not again value ofa + 2
to x, instead say bothx
anda + 2
have same value.
- Erlang's meaning of
1.2 Immutable Data
- Uses persistent data structure (at least for list)
- Each process has its own heap. The data in application is divided in these
heaps and so each individual heap is much much smaller than would have been
the case if all the data had been in single heap.
- as a result garbage collection runs faster.
- if a process terminates before its heap becomes full, all its data is discarded - no garbage collection is required.
1.3 Built in types
- Value types:
- arbitrary sized integers
- size of integers can grow as required
- decimal numbers can be written using
_
to separate groups of 3 digits- Example:
100_000
,1_000_000
- Example:
- floating point numbers
- IEEE 754: 16 digits of accuracy
- atoms
- constants that represents something's name
- written using leading
:
- Example:
:is_binary?
,:tops
- Example:
- written using leading
- constants that represents something's name
- ranges
- start..end, start and end are integers
- regular expression
~{regexp}
, here{}
are delimiters–any non alphanumeric characters can be used as delimiters.- PCRE compatible
- arbitrary sized integers
- System types:
- PIDs and ports
- References
- Collection types:
- Tuples
{1, 2}
,{1, "string", :atoms}
- Common for functions to return tuple where first element in tuple is
:ok
- a little closer to array in terms of implementation
accessing:
elem({12, "thirteen"}, 0)
12
- Lists
- Linked list implementation - O(n) time retrieval complexity.
- List specific operations:
- Concatenation:
[1, 2 , 3] ++ [4, 5, 6]
→[1, 2, 3, 4, 5, 6]
- Difference:
[1, 2, 3, 5] -- [1, 2]
→[3, 5]
- Membership:
1 in [1, 2, 3]
→true
- Keyword list:
- syntactic sugar which takes in
[ name: "Texas", country: "US" ]
and converts to list of 2 value tuples:[{:name, "Texas"}, {:country, "US"}]
- syntactic sugar which takes in
- Concatenation:
- Maps
- Syntax:
%{ key => value, key => value }
If key is an atom, you can use the same shortcut that you use for keyword list:
colors = %{ red: 0xff0000, blue: 0x0000ff, green: 0x00ff00} colors[:read] #gives 0xff0000
- access using
map[key]
syntax- if atoms are keys, you can also use
.
(dot) notationmap.value
- if atoms are keys, you can also use
- Syntax:
- Binaries
- used to access data as sequence of bits and bytes
- helpful to represent JPEG, unicode etc
- Tuples
- Functions
- Truth
true
,false
,nil
(nil
is false)- corresponds to atoms
:true
,:false
,:nil
1.3.1 Operators
there is strict equality operator similar to JavaScript
a === b # 1 == 1.0 is false a == b # 1 == 1.0 is true
- Usual comparison operator included
or
,and
,not
div
gives integer arithmaticrem
is remainder operator, differs from modulo operator in that the result has same sign as functions first argument:(rem(11, 3) → 2)
with
operator is similar to Python'swith
except it works with pattern matching allowing alternate values in case pattern matching fails.
1.4 Anonymous Functions
1.4.1 Syntax
fn (a, b) -> a + b (a, b, c) -> a + b + c end sum = fn (a, b) -> a + b end sum.(1, 2) # gives 3
** (CompileError) iex:1: cannot mix clauses with different arities in anonymous functions (iex) expanding macro: IEx.Helpers.import_file/1 iex:10: (file)
- Use
.
to invoke a annonymous function- Don't need
.
to invoke named function
- Don't need
- When we invoke
sum.(1, 2)
,(1, 2)
is assigned to(a, b)
- assigning is pattern matching
- Elixir tries to match
(1, 2)
with(a, b)
1.4.2 Nested function
functions can be nested
fn -> (fn -> "Hello from inner" end) end
#Function<20.99386804/0 in :erl_eval.expr/5>
They have closures - inner functions remembers their original environment.
prefixer = fn prefix -> fn str -> "#{prefix} #{str}" end end respectify = prefixer.("Sir") respectify.("Tagore")
"Sir Tagore"
1.4.3 &
Notation
syntactic sugar for short helper functions
times_2 = &(&1 * 2) # same as (fn a -> 2 * a) times_2.(12)
24
- Here
&1
,&2
… so on corresponds to first argument, second argument and so on. - If the anonymous function is used to call some other function, Elixir can optimize away the outer call.
Exercises
#Enum.map [1,2,3,4], fn x -> x + 2 end Enum.map [1,2,3,4], &(&1 + 2) #Enum.each [1,2,3,4], fn x -> IO.inspect x end Enum.each [1,2,3,4], &(IO.inspect &1)
1 2 3 4 :ok
1.5 Modules and Named Functions
1.5.1 Function identification
A function is identified by its name and arity
defmodule Times do def double(n) do # double/1 n * 2 end def doubleTrouble(n, m) do # doubleTrouble/2 a = n * 2 m * n * a end end
1.5.2 Function call and pattern matching
defmodule Fibonacci do def of(0), do: 0 def of(1), do: 1 def of(n), do: of(n - 1) + of(n - 2) end Fibonacci.of(15)
610
- Elixir warns about incorrect pattern ordering
defmodule BadFibonacci do def of(n), do: of(n - 1) + of(n - 2) def of(0), do: 0 def of(1), do: 1 end
[33mwarning: [0mthis clause cannot match because a previous clause at line 2 always matches iex:3 [33mwarning: [0mthis clause cannot match because a previous clause at line 2 always matches iex:4 {:module, BadFibonacci, <<70, 79, 82, 49, 0, 0, 4, 12, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 92, 0, 0, 0, 10, 19, 69, 108, 105, 120, 105, 114, 46, 66, 97, 100, 70, 105, 98, 111, 110, 97, 99, 99, 105, 8, 95, 95, ...>>, {:of, 1}}
1.5.3 Guard Clauses
Pattern matching allows Elixir to choose a function based on the arguments passed.
Clauses allow functions to be chosen based on some arbitary predicate. Clauses
are predicates that are attached to function definition using one or more
when
clause.
defmodule Guard do def of(x) when is_number(x) do IO.puts "#{x} is a number" end def of(x) when is_list(x) do IO.puts "#{x} is a list" end def of(x) when is_atom(x) do IO.puts "#{x} is an atom" end end Guard.of(12)
12 is a number :ok
A good use case would be to allow application of
Fibonacci.of
to allow only positive numbers by adding guard:defmodule Fibonacci do # ... rest of methods def of(n) when n > 2 do of(n - 2) + of(n - 1) end end
1.5.4 Default parameters
- default parameters are allowed in any order.
- parameters passed are matched left to right.
Syntax:
defmodule Dparameters do def func(p1, p2 \\ 2, p3 \\ 3, p4) do IO.inspect [p1, p2, p3, p4] end end Dparameters.func("a", "b") Dparameters.func("a", "b", "c") Dparameters.func("a", "b", "c", "d")
["a", 2, 3, "b"] ["a", "b", 3, "c"] ["a", "b", "c", "d"]
1.5.5 Exercise - 6
defmodule Chop do def guess(actual, low..high) when (low <= actual and low <= high) do guess_helper(actual, low..high) end def guess_helper(actual, low..high) when (actual < div(low + high, 2)) do IO.puts "Is it #{div(low + high, 2)}" guess_helper(actual, low..div(low + high, 2)) end def guess_helper(actual, low..high) when (actual == div(low + high, 2)) do div(low + high, 2) end def guess_helper(actual, low..high) when (div(low + high, 2) < actual) do IO.puts "Is it #{div(low + high, 2)}" guess_helper(actual, div(low + high, 2)..high) end end Chop.guess(273, 1..1000)
Is it 500 Is it 250 Is it 375 Is it 312 Is it 281 Is it 265 273
- Note that we have not used
if
statements yet.
1.5.6 Private functions
defp
defines private function – a function that can be called only within a moduledefp
is a macro- all function with multiple heads should be either public or private but not mixed
Syntax
defp priFun(n), do: IO.puts("I'm a private fun")
1.5.7 Pipe Operator |>
|>
operator takes result of expression on left and inserts it as the first parameter of the function invocation to its right.- We can provide 2nd arguments if needed
syntax
top_10_marks = get_top(get_marks(get_students(:class9)), 10) # can be written as top_10_marks = get_students(:class9) |> get_marks |> get_top(10)
1.5.8 Modules
- can be nested
- call function in nested module as
OuterModule.InnerModule.funName
- module nesting is an illusion – Elixir puts all modules in global namespace and prepends outer module name to inner module name with a "."
import
puts all function in module in current scope.import List, only: [flatten: 1, duplicate: 2] # optional second parameter lets you control which functions or macros are # imported. # you can use only: or except:
1.5.9 Alias
we can make alias for modules, specially nested modules
alias OuterModule.Innermodule.InnerModule.Parser, as: Parser
1.5.10 Module attributes
- each module have associated metadata. Each item is called attribute
@name value
attributes can be defined at top level and can be accessed at any level
defmodule CoolModule do @author "Anurag Peshne" def author do @author end end CoolModule.author
"Anurag Peshne"
- Attributes are not variables. Use them for configuration and metadata only.
- Can be used instead of constants.
- Attributes are not variables. Use them for configuration and metadata only.
Internally, module names are just atoms. When you write a name starting with an uppercase letter, Elixir converts it internally into an atom.
IO
gets converted toElixir.IO
IO.puts(is_atom IO) IO.puts(to_string IO) IO.puts :"Elixir.IO" == IO # so IO can be written as :"Elixir.IO".puts("hello real module")
true Elixir.IO true hello real module :ok
1.5.11 Interoperability
- Erlang variables starts with an uppercase letter
- and atoms are lowercase names
- To call Erlang's module, say
timer
, use atom:timer
in Elixir Erlang's
crypto
:crypto.hash(:md5, "input string") |> Base.encode16()
"164C375B4A5DF44A332CA34BDA6CBA9D"
1.6 Lists and Recursion
- List consists of head and tail which is a list itself.
list can be denoted using
|
operator, which divides heads and tails. This can be helpful in pattern matching.[a, b, c] = [1, 2, 3] IO.puts "#{a}, #{b}, #{c}" [head | tail] = [1, 2, 3] IO.puts head tail
1, 2, 3 1 [2, 3]
Example: sum of list
defmodule ListOps do def sum([]) do 0 end def sum([head | tail]) do head + sum(tail) end end ListOps.sum([1,2,3,4,5])
15
- This is classic ML
1.6.1 Cons
take a list and return list of square
defmodule ListOps do def sqList([]) do [] end def sqList([head | tail]) do [head * head | sqList(tail)] end end ListOps.sqList([1,2,3,4])
[1, 4, 9, 16]
Map:
defmodule MyMap do def map([], _func), do: [] def map([head | tail], func), do: [func.(head) | map(tail, func)] end MyMap.map([1,2,3,4], &(&1 * &1))
[1, 4, 9, 16]
- Note the
.
during application offunc
in body ofmap
.
- Note the
1.7 Maps
1.7.1 Maps
- keyword lists
- map:
%{name: value}
1.7.2 Structs
- a map that has fixed set of fields and default values for those fields, and we can pattern match by type as well as content.
The name of the module is name of the map type. Inside module use
defstruct
macro to define the struct's member.defmodule Subscriber do defstruct name: "", paid: false, over_18: true end
- we can add struct specific functions in module.
1.8 Processing Collections
1.8.1 Enums
- greedy
1.8.2 Stream
- lazy
- Infinite stream possible
1.9 Strings
heredocs
a_string = " a string can span multiple lines "
- Sigils
- a symbol with magical powers
- A sigil starts with
~
followed by upper case or lowercase letter~D
date~r
regular expression etc
1.9.1 Single quote and double quote
- double quote are strings
- double quoted strings are binaries
- single quote are character list
1.10 Control Flow
1.10.1 if
and unless
if <condition> do: <statments> [else: statements]
unless <condition> do: <statments> [else: statments]
if
andunless
act opposite:unless 1 == 2 do :ok else "Error" end
:ok
1.10.2 cond
- similar to lisp
cond
:takes in series of condition and evaluates them:
val = 2 cond do val == 1 -> "one" val == 2 -> "two" val == 3 -> "three" end
"two"
1.10.3 case
matches value against set of patterns
val = 3 case val do 1 -> "One" 2 -> "Two" 3 -> "Three" end
"Three"
1.11 Exceptions
raise "Fatal!" # raises RuntimeError raise RuntimeError, message: "Fatal runtime error"
1.11.1 Handling Exception
case File.open("non_existing_file") do {:ok, file} -> process(file) {:error, message} -> IO.puts :stderr, "Couldn't open file" end