2018/4/2

Elixir 2 - Modules, Functions


Anonymous Functions


fn
    parameter-list -> body
    parameter-list -> body ...
end

iex(1)> sum = fn (a, b) -> a + b end
#Function<12.99386804/2 in :erl_eval.expr/5>
iex(2)> sum.(1, 2)
3

# 可省略 ()
iex(3)> sum = fn a, b -> a + b end
#Function<12.99386804/2 in :erl_eval.expr/5>

# 沒有參數的 fn
iex(5)> f2 = fn -> 99 end
#Function<20.99386804/0 in :erl_eval.expr/5>
iex(6)> f2.()
99

以 tuple 為參數


iex(8)> swap = fn { a, b } -> { b, a } end
#Function<6.99386804/1 in :erl_eval.expr/5>
iex(9)> swap.({1,2})
{2, 1}



一個 fn 可以有多種 function body,fn 是以 File.open 回傳的結果決定 fn body 的內容是使用哪一個 pattern 的定義


iex(1)> open_file = fn
...(1)>     {:ok, file} -> "Read data: #{IO.read(file, :line)}"
...(1)>     {_, error} -> "Error: #{:file.format_error(error)}"
...(1)>   end
#Function<6.99386804/1 in :erl_eval.expr/5>
iex(2)> open_file.(File.open("/etc/passwd"))
"Read data: ##\n"
iex(3)>   open_file.(File.open("nonexistent"))
"Error: no such file or directory"



可以 Return Function


iex(6)> fun1 = fn -> fn -> "Hello" end end
### 等同 fun1 = fn -> (fn -> "Hello" end) end
#Function<20.99386804/0 in :erl_eval.expr/5>
iex(7)> fun1.()
#Function<20.99386804/0 in :erl_eval.expr/5>
iex(8)> fun1.().()
"Hello"



Functions Remember Their Original Environment


iex(13)> greeter = fn name -> (fn -> "Hello #{name}" end) end
#Function<6.99386804/1 in :erl_eval.expr/5>
iex(14)> dave_greeter = greeter.("Dave")
#Function<20.99386804/0 in :erl_eval.expr/5>
iex(15)> dave_greeter.()
"Hello Dave"



Parameterized Functions


iex(19)> add_n = fn n -> (fn other -> n + other end) end
#Function<6.99386804/1 in :erl_eval.expr/5>
iex(20)> add_two = add_n.(2)
#Function<6.99386804/1 in :erl_eval.expr/5>
iex(21)> add_five = add_n.(5)
#Function<6.99386804/1 in :erl_eval.expr/5>
iex(22)> add_two.(3)
5
iex(23)> add_five.(7)
12



以 fun 為參數


iex(26)> times_2 = fn n -> n * 2 end
#Function<6.99386804/1 in :erl_eval.expr/5>
iex(27)> apply = fn (fun, value) -> fun.(value) end
#Function<12.99386804/2 in :erl_eval.expr/5>
iex(28)> apply.(times_2, 6)
12



利用 Pin operator 將 fun 固定參數


defmodule Greeter do
  def for(name, greeting) do
    fn
      (^name) -> "#{greeting} #{name}"
      (_) -> "I don't know you"
    end
  end
end
mr_greeter = Greeter.for("Test", "You")
IO.puts mr_greeter.("Test") # => You Test
IO.puts mr_greeter.("Dave") # => I don't know you



利用 & 實作 func


add_one = &(&1 + 1)
# & 就是 fn
# &1 是 fn 的第一個參數, &2 是 fn 的第二個參數

### 等同
add_one = fn (n) -> n + 1 end

iex(38)> divrem = &{ div(&1,&2), rem(&1,&2) }
#Function<12.99386804/2 in :erl_eval.expr/5>
iex(39)> divrem.(13, 5)
{2, 3}

iex(42)> l = &length/1
&:erlang.length/1
iex(43)> l.([1,3,5,7])
4

& 可快速產生一個 anonymous function 傳入 map function 的第二個參數


iex(44)> Enum.map [1,2,3,4], &(&1 + 1)
[2, 3, 4, 5]

Modules & Named Functions


times.exs


defmodule Times do
  def double(n) do
    n * 2
  end
end

$ iex times.exs
iex(1)> Times.double
double/1
iex(1)> Times.double(3)
6

也可以啟動 iex 後,用 c 編譯


iex(1)> c("times.exs")
[Times]

## 可直接知道是哪一行,哪一個 function 發生錯誤
iex(2)> Times.double("2")
** (ArithmeticError) bad argument in arithmetic expression
    times.exs:3: Times.double/1

do: 是 syntax sugar,可以用 () 將 expression grouping 在一起, do: 就是 do ... end 的簡化語法


defmodule Times do
  def double(n), do: n * 2
  
  def greet(greeting, name), do: (
    IO.puts greeting
    IO.puts "How're you doing, #{name}?"
  )
end



Function Call with Pattern Matching


defmodule Factorial do
  def of(0), do: 1
  def of(n), do: n * of(n-1)
end

iex(1)> Factorial.of(3)
6
iex(2)> Factorial.of(100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

def of(0), do: 1 不能寫在後面,因為 elixir 是依照順序檢查是否有符合 pattern,如果寫錯了,compiler 會出現警告


defmodule BadFactorial do
  def of(n), do: n * of(n-1)
  def of(0), do: 1
end

warning: this clause cannot match because a previous clause at line 2 always matches
  factorial1-bad.exs:3



Guard Clauses


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

回頭看剛剛的 factorial,如果 n 為負數,就會發生無窮迴圈的狀況,因此要改為


defmodule Factorial do
  def of(0), do: 1
  def of(n) when n > 0 do
    n * of(n-1)
  end
end

在 guard clause 中可以使用的 elixir expression


  1. comparison operator


    ==, !=, ===, !==, >, <, <=, >=

  2. Boolean and negation operators


    or, and, not, !
    不能使用 || and &&

  3. Arithmetic operators


    +, -, *, /

  4. Join operators


    <> and ++, as long as the left side is a literal.

  5. in operator


    Membership in a collection or range

  6. Type-check functions


    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
  7. Other functions


    回傳 value 的 built-in functions


    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)



參數的預設值


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"]

因為前面已經定義了 2, 3, 4 個參數的 func,因此 def func(p1, p2) 會出現重複 func 定義的 error


defmodule Example do

  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

end


** (CompileError) default_params.exs:7: def func/2 conflicts with defaults from def func/4
    default_params.exs:7: (module)
    default_params.exs:1: (file)

以沒有 func body 的 func 定義,來解決剛剛遇到的問題


defmodule Params do
  def func(p1, p2 \\ 123)

  def func(p1, p2) when is_list(p1) do
    "You said #{p2} with a list"
  end

  def func(p1, p2) do
    "You passed in #{p1} and #{p2}"
    end
end

IO.puts Params.func(99) # You passed in 99 and 123
IO.puts Params.func(99, "cat") # You passed in 99 and cat
IO.puts Params.func([99]) # You said 123 with a list
IO.puts Params.func([99], "dog") # You said dog with a list



defp 可定義 Private Functions,但不能 private 與 public function 不能共用




Pipe Operator |>


以往會寫成


people = DB.find_customers
orders = Orders.for_customers(people)
tax = sales_tax(orders, 2016)
filing = prepare_filing(tax)

或是


filing = prepare_filing(sales_tax(Orders.for_customers(DB.find_customers), 2016))

但有了 |> 可改成


filing = DB.find_customers
    |> Orders.for_customers
    |> sales_tax(2016)
    |> prepare_filing

val |> f(a,b)

等同
f(val,a,b)

list
    |> sales_tax(2016)
    |> prepare_filing

等同

prepare_filing(sales_tax(list, 2016))

& 就是 fun


iex> (1..10) |> Enum.map(&(&1*&1)) |> Enum.filter(&(&1 < 40))
[1, 4, 9, 16, 25, 36]



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

Inner Module


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

mix/tasks/doctest.ex


defmodule Mix.Tasks.Doctest do
    def run do
    end
end

Mix.Tasks.Doctest.run

Directives for Modules


  1. import


    語法 import Module [, only:|except: ]


    ex: import List, only: [ flatten: 1, duplicate: 2 ]


    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
  2. alias


    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

    也可寫成


    alias My.Other.Module.Parser
    alias My.Other.Module.Runner
    
    alias My.Other.Module.{Parser, Runner}
  3. require


    當寫下 require a module,在編譯時可保證一定存在該 macrio 定義




Module Attribute


只能用在 module 的最底層,這不是變數,比較接近 configuration, metadata 的用途


defmodule Example do

    @author  "Test"
    def get_author do
        @author
    end

    @attr "one"
    def first, do: @attr
    @attr "two"
    def second, do: @attr

end

module name 就是 atoms,因此 IO 會被轉換為 Elixir.IO


iex(1)> is_atom IO
true
iex(2)> to_string IO
"Elixir.IO"
iex(3)> :"Elixir.IO" === IO
true
iex(4)> IO.puts 123
123
:ok
iex(5)> :"Elixir.IO".puts 123
123
:ok



如何呼叫 Erlang Library 的 Functions


erlang 的 io module,就是 elixir 的一個 atom :io


iex(16)> :io.format("The number is ~3.1f~n", [5.678])
The number is 5.7

References


Programming Elixir