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 say x = a + 2, we do not again value of a + 2 to x, instead say both x and a + 2 have same value.

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
    • floating point numbers
      • IEEE 754: 16 digits of accuracy
    • atoms
      • constants that represents something's name
        • written using leading :
          • Example: :is_binary?, :tops
    • 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
  • 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"}]
    • 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) notation map.value
    • Binaries
      • used to access data as sequence of bits and bytes
      • helpful to represent JPEG, unicode etc
  • 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 arithmatic
  • rem 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's with 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
  • 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
  1. 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
    
    warning: this clause cannot match because a previous clause at line 2 always matches
      iex:3
    
    warning: this 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 module
  • defp 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.
  • 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 to Elixir.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 of func in body of map.

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 and unless 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

1.12 Mix: Project Organization

Author: Anurag Peshne

Created: 2019-03-22 Fri 22:23

Emacs 26.1 (Org mode 9.2.1)

Validate