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