2018/04/30

Elixir 6 MultipleProcesses

elixir 的 process 並不是 OS process,而是 erlang VM process。Elixir 使用 actor model for concurrency,actor 是一個獨立的 process,我們可 spawn prcess,發送及接收 messages。

Process

spawn-basic.ex

defmodule SpawnBasic do
  def greet do
    IO.puts "Hello"
  end
end

spawn 產生一個新的 process,#PID<0.93.0> 是 process id

iex(1)> c("spawn-basic.ex")
[SpawnBasic]
iex(2)> SpawnBasic.greet
Hello
:ok
iex(3)> spawn(SpawnBasic, :greet, [])
Hello
#PID<0.93.0>

在 Process 之間傳遞 Message

defmodule Spawn1 do
  def greet do
    receive do
      {sender, msg} ->
        send( sender, { :ok, "Hello #{msg}" } )
    end
  end
end

# here's a client 發送訊息給 pid,self 是 caller's PID
pid = spawn(Spawn1, :greet, [])
send pid, {self(), "World!"}

receive do
  {:ok, message} ->
    IO.puts message
end

這是舊的語法,現在要改為 send( pid, msg )

pid <- {self, "World!"}

send self(), "World!"
send( self, "World!")

發送多個訊息

defmodule Spawn2 do
  def greet do
    receive do
      {sender, msg} ->
        send( sender, { :ok, "Hello #{msg}" } )
    end
  end
end

# here's a client
pid = spawn(Spawn2, :greet, [])

send pid, {self(), "World!"}
receive do
  {:ok, message} ->
    IO.puts message
end

send pid, {self(), "Kermit!"}
receive do
  {:ok, message} ->
    IO.puts message
end

執行後,程式會停在這裡,這是因為,發送第一個訊息,Spawn2 greet 處理後,回傳結果後就結束了,所以當發送第二個訊息給 pid後,等待回應時,一直無法收到回傳的訊息。

Hello World!

改寫:在 receive 的部分,設定 timeout

defmodule Spawn2 do
  def greet do
    receive do
      {sender, msg} ->
        send( sender, { :ok, "Hello #{msg}" } )
    end
  end
end

# here's a client
pid = spawn(Spawn2, :greet, [])

send pid, {self(), "World!"}
receive do
  {:ok, message} ->
    IO.puts message
end

send pid, {self(), "Kermit!"}
receive do
  {:ok, message} ->
    IO.puts message
  after 500 ->
    IO.puts "The greeter has gone away"
end

執行結果

Hello World!
The greeter has gone away

最正確的寫法,應該要讓 Spawn 不斷接收並處理訊息

defmodule Spawn4 do
  def greet do
    receive do
      {sender, msg} ->
        send( sender, { :ok, "Hello #{msg}" } )
        greet
    end
  end
end

# here's a client
pid = spawn(Spawn4, :greet, [])

send pid, {self(), "World!"}
receive do
  {:ok, message} ->
    IO.puts message
end

send pid, {self(), "Kermit!"}
receive do
  {:ok, message} ->
    IO.puts message
  after 500 ->
    IO.puts "The greeter has gone away"
end
Hello World!
Hello Kermit!

Tail Recursion: 尾遞迴

defmodule TailRecursive do

  def factorial(n), do: _fact(n, 1)
  
  defp _fact(0, acc), do: acc
  defp _fact(n, acc), do: _fact(n-1, acc*n)

end

Process Overhead

這是一個用來測試 process 效能的小程式,一開始會產生 n 個 processes,第一個會送數字給第二個,加 1 後傳給第三個,最後一個會回傳結果給第一個。

defmodule Chain do

  def counter(next_pid) do
    receive do
      n ->
        send next_pid, n + 1
    end
  end

  def create_processes(n) do
    last = Enum.reduce 1..n, self(),
             fn (_,send_to) ->
                ## 產生 process 執行 :counter,將自己的 process PID 送到 send_to 由 spawn 傳給下一個 process
               spawn(Chain, :counter, [send_to])
             end

    # start the count by sending
    send last, 0

    # and wait for the result to come back to us
    receive do
      final_answer when is_integer(final_answer) ->
        "Result is #{inspect(final_answer)}"
    end
  end

  def run(n) do
    IO.puts inspect :timer.tc(Chain, :create_processes, [n])
  end

end
$ elixir -r chain.exs -e "Chain.run(10)"
{3932, "Result is 10"}

$ elixir -r chain.exs -e "Chain.run(1_000)"
{15951, "Result is 1000"}

$ elixir -r chain.exs -e "Chain.run(40_000)"
{444129, "Result is 40000"}

$ elixir -r chain.exs -e "Chain.run(50_000)"
{545041, "Result is 50000"}

$ elixir -r chain.exs -e "Chain.run(300_000)"

10:54:49.904 [error] Too many processes
** (SystemLimitError) a system limit has been reached

調整 vm 參數 --erl "+P 1000000"

$ elixir --erl "+P 1000000" -r chain.exs -e "Chain.run(300_000)"
{2924026, "Result is 300000"}

When Process Die

預設狀況下,沒有人會知道 Process 結束了

import :timer, only: [ sleep: 1 ]

defmodule Link1 do
  def sad_method do
    sleep 500
    exit(99)
  end

  def run do
    spawn(Link1, :sad_method, [])

    receive do
      msg ->
        IO.puts "MESSAGE RECEIVED: #{inspect msg}"
    after 1000 ->
        IO.puts "Nothing happened as far as I am concerned"
    end
  end
end

Link1.run

Link1 不知道新的 sad_method process 已經結束了,在 1s 後 timeout

$ elixir -r link1.exs
Nothing happened as far as I am concerned

要解決上面的問題,可以用 spawn_link 連結兩個 Process,當 child process 因異常死亡時,會連帶把另一個 process 停掉。

import :timer, only: [ sleep: 1 ]

defmodule Link2 do
  def sad_method do
    sleep 500
    exit(99)
  end

  def run do
    spawn_link(Link2, :sad_method, [])

    receive do
      msg ->
        IO.puts "MESSAGE RECEIVED: #{inspect msg}"
    after 1000 ->
        IO.puts "Nothing happened as far as I am concerned"
    end
  end
end

Link2.run
$ elixir -r link2.exs
** (EXIT from #PID<0.73.0>) 99
defmodule Link3 do
  import :timer, only: [ sleep: 1 ]

  def sad_function do
    sleep 500
    exit(:boom)
  end
  def run do
    # 將 :trap_exit signal 轉換為 message :EXIT
    Process.flag(:trap_exit, true)
    
    spawn_link(Link3, :sad_function, [])
    receive do
      msg ->
        IO.puts "MESSAGE RECEIVED: #{inspect msg}"
    after 1000 ->
        IO.puts "Nothing happened as far as I am concerned"
    end
  end
end

Link3.run
$ elixir -r link3.exs
MESSAGE RECEIVED: {:EXIT, #PID<0.79.0>, :boom}

Monitoring a Process

spawnmonitor 可在 spawna process 時連帶啟用 monitoring,或是使用 Process.monior 監控已經存在的 process。但如果是用 Process.monitor,可能會產生 race condition,如果其他 process 在呼叫 monitor call 完成前就死亡,就會收不到 notification。 spawnlink 及 spawn_monitor versions 為 atomic,所以可以 catch a failure。

import :timer, only: [ sleep: 1 ]

defmodule Monitor1 do
  def sad_method do
    sleep 500
    exit(99)
  end

  def run do
    res = spawn_monitor(Monitor1, :sad_method, [])
    IO.puts inspect res

    receive do
      msg ->
        IO.puts "MESSAGE RECEIVED: #{inspect msg}"
    after 1000 ->
        IO.puts "Nothing happened as far as I am concerned"
    end
  end
end

Monitor1.run
$ elixir -r monitor1.exs
{#PID<0.79.0>, #Reference<0.759175825.870842370.239990>}
MESSAGE RECEIVED: {:DOWN, #Reference<0.759175825.870842370.239990>, :process, #PID<0.79.0>, 99}

結果跟 spawnlink 差不多,如果是用 spawnlink,子 process crash 連帶會影響 monitor process,如果用spawn_monitor,就會知道 crash 的原因。

Parallel Map

通常 map 會回傳一個 apply function 到 collection 裡面每一個 elements 的 list,parallel map 的功能一樣,但會在分別獨立的 process 對每一個 element 都 apply function。

首先會將 collection map 到 fn,在 fn 內 spawn_link 產生 a list of PIDs,每一個 PID 都會對每一個 element 執行給定的 function。

第二個 |> 會將 list of PIDs 轉成 results,各自傳給 list 內每一個PID,注意 ^pid 可讓 receive 依照順序取得結果。

defmodule Parallel do
  def pmap(collection, fun) do
    me = self()

    collection
  |>
    Enum.map(fn (elem) ->
       spawn_link fn -> ( send me, { self(), fun.(elem) } ) end
     end)
  |>
    Enum.map(fn (pid) ->
      receive do { ^pid, result } -> result end
    end)
  end
end
iex(1)> Parallel.pmap 1..10, &(&1 * &1)
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

Fibonacci Server

當 calculator 可處理下一個數字時,會發送 :ready 給 scheduler,scheduler 會以 :fib message 發送計算工作給 calculator,calculator 會以 :answer 回傳結果,scheduler 會發送 :shutdown 給 calculator 通知要 exit。

defmodule FibSolver do

  def fib(scheduler) do
    # 啟動後,發送 :ready 給 scheduler,還有自己的 PID
    send scheduler, { :ready, self() }
    receive do
      { :fib, n, client } ->
        # 計算 n 的 fib 結果,回傳結果給 client
        send client, { :answer, n, fib_calc(n), self() }
        # 等待要處理的下一個 message
        fib(scheduler)
      { :shutdown } ->
        # 收到 shutdown 就結束工作
        exit(0)
    end
  end

  # very inefficient, deliberately
  defp fib_calc(0), do: 1
  defp fib_calc(1), do: 1
  defp fib_calc(n), do: fib_calc(n-1) + fib_calc(n-2)
end

defmodule Scheduler do

  # 由 run 啟動 scheduler
  def run(num_processes, module, func, to_calculate) do
    # 產生 num_processes 個 process,送入 scheduler_processes
    (1..num_processes)
    |> Enum.map(fn(_) -> spawn(module, func, [self()]) end)
    |> schedule_processes(to_calculate, [])
  end

  defp schedule_processes(processes, queue, results) do
    receive do
      {:ready, pid} when length(queue) > 0 ->
        [ next | tail ] = queue
        send pid, {:fib, next, self()}
        schedule_processes(processes, tail, results)

      {:ready, pid} ->
        # queue 裡面已經沒有要計算的工作,就發送 :shutdown 給該 process
        send pid, {:shutdown}
        if length(processes) > 1 do
          # 如果 processes 裡面還有 process,就從 processes 中去掉這個 pid,再迴圈繼續等待其他還沒執行完成的 processes
          schedule_processes(List.delete(processes, pid), queue, results)
        else
          # 已經沒有 processes,所有 processes 都已經 shutdown,就排序結果
          Enum.sort(results, fn {n1,_}, {n2,_}  -> n1 <= n2 end)
        end

      # 接收 fib 計算的結果
      {:answer, number, result, _pid} ->
        schedule_processes(processes, queue, [ {number, result} | results ])

    end
  end
end

to_process = [27, 33, 35, 11, 36, 29, 18, 37, 21, 31, 19, 10, 14, 30,
              15, 17, 23, 28, 25, 34, 22, 20, 13, 16, 32, 12, 26, 24]

Enum.each 1..10, fn num_processes ->
  {time, result} = :timer.tc(Scheduler, :run,
                               [num_processes, FibSolver, :fib, to_process])
  if num_processes == 1 do
    IO.puts inspect result
    IO.puts "\n #   time (s)"
  end
  :io.format "~2B     ~.2f~n", [num_processes, time/1000000.0]
end

執行結果

[{10, 89}, {11, 144}, {12, 233}, {13, 377}, {14, 610}, {15, 987}, {16, 1597}, {17, 2584}, {18, 4181}, {19, 6765}, {20, 10946}, {21, 17711}, {22, 28657}, {23, 46368}, {24, 75025}, {25, 121393}, {26, 196418}, {27, 317811}, {28, 514229}, {29, 832040}, {30, 1346269}, {31, 2178309}, {32, 3524578}, {33, 5702887}, {34, 9227465}, {35, 14930352}, {36, 24157817}, {37, 39088169}]

 #   time (s)
 1     3.38
 2     1.99
 3     1.93
 4     1.75
 5     1.73
 6     1.88
 7     1.76
 8     1.71
 9     1.72
10     1.81

目前這個版本的計算過程如下,有很多重複的計算

fib(5)
= fib(4) + fib(3)
= fib(3) + fib(2) + fib(2) + fib(1)
= fib(2) + fib(1) + fib(1) + fib(0) + fib(1) + fib(0) + fib(1)
= fib(1) + fib(0) + fib(1) + fib(1) + fib(0) + fib(1) + fib(0) + fib(1)

Elixir module 不能儲存資料,但 process 可以儲存 state,elixir 提供 Agent module,可封裝 process 包含了 state。

fib_agent.exs

defmodule FibAgent do
  def start_link do
    Agent.start_link(fn -> %{ 0 => 0, 1 => 1 } end)
  end

  def fib(pid, n) when n >= 0 do
    Agent.get_and_update(pid, &do_fib(&1, n))
  end

  defp do_fib(cache, n) do
    case cache[n] do
      nil ->
        { n_1, cache } = do_fib(cache, n-1)
        result         = n_1 + cache[n-2]
        { result, Map.put(cache, n, result) }

      cached_value ->
        { cached_value , cache }
    end
  end

end

{:ok, agent} = FibAgent.start_link()
IO.puts FibAgent.fib(agent, 2000)

Nodes

erlang Beam VM 可處理自己的 event, process scheduling, memory, naming service, interprocess communication,nodes 可互相連接。

查詢 VM node name

iex(1)> Node.self
:nonode@nohost

可在啟動 iex 時以 --name 或 --sname 設定 node name

$ iex --name name1@cmbp.local
iex(name1@cmbp.local)1> Node.self
:"name1@cmbp.local"

$ iex --name name1
iex(name1@cmbp.local)1> Node.self
:"name1@cmbp.local"

可使用 Node.connect :"name1@cmbp" 連接另一個 node

$ iex --sname name1
-----
$ iex --sname name2

iex(name2@cmbp)1> Node.list
[]
iex(name2@cmbp)2> Node.connect :"name1"
false
iex(name2@cmbp)3> Node.connect :"name1@cmbp.local"

23:32:10.451 [error] ** System NOT running to use fully qualified hostnames **
** Hostname cmbp.local is illegal **

false
iex(name2@cmbp)4> Node.connect :"name1@cmbp"
true
iex(name2@cmbp)5> Node.list
[:name1@cmbp]
# 先產生一個 fun
iex(name1@cmbp)1> func = fn -> IO.inspect Node.self end
#Function<20.99386804/0 in :erl_eval.expr/5>

iex(name1@cmbp)2> spawn(func)
:name1@cmbp
#PID<0.96.0>

# 在 name1@cmbp 產生 func process
iex(name1@cmbp)3> Node.spawn(:"name1@cmbp", func)
:name1@cmbp
#PID<0.98.0>

# 在 name2@cmbp 產生 func process
# 雖然 process 在 name2,IO 還是在 name1
iex(name1@cmbp)4> Node.spawn(:"name2@cmbp", func)
#PID<9755.103.0>
:name2@cmbp

Nodes, Cookies, and Security

在啟動 VM 時,增加 cookie 設定,可增加安全性,相同 cookie 的 node 才能連接在一起。

$ iex --sname name2 --cookie cookie2
iex(name2@cmbp)1>
00:21:31.987 [error] ** Connection attempt from disallowed node :name1@cmbp **

nil
iex(name2@cmbp)2> Node.get_cookie
:cookie2

-------

$ iex --sname name1 --cookie cookie1
iex(name1@cmbp)1> Node.connect :"name2@cmbp"
false

erlang VM 預設會讀取 ~/.erlang.cookie 這個檔案的 cookie

Process Name

PID 有三個部分的數字,但只有兩個 fields: 第一個數字是 Node ID,後兩個數字是 low and high bits of the process ID,如果 export PID 到另一個 node,node ID 會設定為 process 存在的 node number。

iex(name2@cmbp)3> self()
#PID<0.90.0>

process 以 :global.register_name(@name, pid) 註冊 process name,後續就可以用 :global.whereis_name(@name) 找到這個 process。

如果在程式裡註冊 global process name,可能會遇到名稱一樣的問題,可以改用 mix.exs 設定檔,管理要註冊到 global state 的 process names。

defmodule Tick do

  @interval 2000   # 2 seconds

  @name  :ticker

  def start do
    pid = spawn(__MODULE__, :generator, [[]])
    :global.register_name(@name, pid)
  end

  def register(client_pid) do
    send :global.whereis_name(@name), { :register, client_pid }
  end

  def generator(clients) do
    receive do
      { :register, pid } ->
        IO.puts "registering #{inspect pid}"
        generator([pid|clients])

    after
      @interval ->
        IO.puts "tick"
        Enum.each clients, fn client ->
          send client, { :tick }
        end
        generator(clients)
    end
  end
end

defmodule Client do

  def start do
    pid = spawn(__MODULE__, :receiver, [])
    Tick.register(pid)
  end

  def receiver do
    receive do
      { :tick } ->
        IO.puts "tock in client"
        receiver()
    end
  end
end
$ iex --sname name1
iex(name1@cmbp)1> c("ticker.ex")
[Client, Tick]
iex(name1@cmbp)2> Node.connect :"name2@cmbp"
true
iex(name1@cmbp)3> Tick.start
:yes
tick
tick
tick
iex(name1@cmbp)4> Client.start
registering #PID<0.111.0>
{:register, #PID<0.111.0>}
tick
tock in client
tick
tock in client

-------
$ iex --sname name2
iex(name2@cmbp)1> c("ticker.ex")
[Client, Tick]
iex(name2@cmbp)2> Client.start
{:register, #PID<0.104.0>}
tock in client
tock in client
tock in client

I/O, PIDs, and Nodes

Erlang VM 將 I/O 實作為 processes。可以直接透過 I/O server 的PID 對 open file/device 處理 IO。

VM 的 default IO device 可透過 :erlang.group_leader 取得,他會回傳 I/O Server 的 PID

$ iex --sname name1

-------

$ iex --sname name2
iex(name2@cmbp)1> Node.connect(:"name1@cmbp")
true
iex(name2@cmbp)2> :global.register_name(:name2, :erlang.group_leader)
:yes

------

# 回到 :name1
iex(name1@cmbp)1> name2 = :global.whereis_name :name2
#PID<9755.59.0>
iex(name1@cmbp)2> IO.puts(name2, "test")
:ok

------

# :name2 的 IO 可看到

test

References

Programming Elixir

2018/04/23

Elixir5 ProjectTool

mix

mix 是管理 elixir project 的工具

$ mix help
mix                   # Runs the default task (current: "mix run")
mix app.start         # Starts all registered apps
mix app.tree          # Prints the application tree
mix archive           # Lists installed archives
mix archive.build     # Archives this project into a .ez file
mix archive.install   # Installs an archive locally
mix archive.uninstall # Uninstalls archives
mix clean             # Deletes generated application files
mix cmd               # Executes the given command
mix compile           # Compiles source files
mix deps              # Lists dependencies and their status
mix deps.clean        # Deletes the given dependencies' files
mix deps.compile      # Compiles dependencies
mix deps.get          # Gets all out of date dependencies
mix deps.tree         # Prints the dependency tree
mix deps.unlock       # Unlocks the given dependencies
mix deps.update       # Updates the given dependencies
mix do                # Executes the tasks separated by comma
mix escript           # Lists installed escripts
mix escript.build     # Builds an escript for the project
mix escript.install   # Installs an escript locally
mix escript.uninstall # Uninstalls escripts
mix help              # Prints help information for tasks
mix loadconfig        # Loads and persists the given configuration
mix local             # Lists local tasks
mix local.hex         # Installs Hex locally
mix local.public_keys # Manages public keys
mix local.rebar       # Installs Rebar locally
mix new               # Creates a new Elixir project
mix profile.cprof     # Profiles the given file or expression with cprof
mix profile.fprof     # Profiles the given file or expression with fprof
mix run               # Runs the given file or expression
mix test              # Runs a project's tests
mix xref              # Performs cross reference checks
iex -S mix            # Starts IEx and runs the default task

建立新 project

$ mix new test
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/test.ex
* creating test
* creating test/test_helper.exs
* creating test/test_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd test
    mix test

Run "mix help" for more commands.

如果要放到 git

$ git init
$ git add .
$ git commit -m "Initial commit of new project"

project 結構:

config/
  內含 config.exs, 設定資料
lib/
  內含 test.ex,這是 top-level module
mix.exs
  project 設定
README.md
  project 說明文件
test/
  內含 test_helper.exs, test_test.exs 測試程式

如何處理 command line

lib/issues/cli.ex

defmodule Issues.CLI do

  @default_count 4

  @moduledoc """
  Handle the command line parsing and the dispatch to
  the various functions that end up generating a
  table of the last _n_ issues in a github project
  """


  def run(argv) do
    parse_args(argv)
  end

  @doc """
  `argv` can be -h or --help, which returns :help.

  Otherwise it is a github user name, project name, and (optionally)
  the number of entries to format.

  Return a tuple of `{ user, project, count }`, or `:help` if help was given.
  """
  def parse_args(argv) do
    parse = OptionParser.parse(argv, switches: [ help: :boolean],
                                     aliases:  [ h:    :help   ])
    case  parse  do

    { [ help: true ], _,           _ } -> :help
    { _, [ user, project, count ], _ } -> { user, project, String.to_integer(count) }
    { _, [ user, project ],        _ } -> { user, project, @default_count }
    _                                  -> :help

    end
  end
end

簡單的測試 test/issues_test.exs

defmodule IssuesTest do
  use ExUnit.Case

  test "the truth" do
    assert(true)
  end
end

test/cli_test.exs

defmodule CliTest do
  use ExUnit.Case

  test "nil returned by option parsing with -h and --help options" do
    assert Issues.CLI.parse_args(["-h",     "anything"]) == :help
    assert Issues.CLI.parse_args(["--help", "anything"]) == :help
  end

  test "three values returned if three given" do
    assert Issues.CLI.parse_args(["user", "project", "99"]) == { "user", "project", 99 }
  end

  test "count is defaulted if two values given" do
    assert Issues.CLI.parse_args(["user", "project"]) == { "user", "project", 4 }
  end
end

mix.exs

defmodule Issues.Mixfile do
  use Mix.Project

  def project do
    [ app:     :issues,
      version: "0.0.1",
      deps:    deps 
    ]
  end

  # Configuration for the OTP application
  def application do
    []
  end

  # Returns the list of dependencies in the format:
  # { :foobar, "0.1", git: "https://github.com/elixir-lang/foobar.git" }
  defp deps do
    []
  end
end

列印 project deps 相關 libs

$ mix deps

取得 deps

$ mix deps.get

執行測試

$ mix test
warning: variable "deps" does not exist and is being expanded to "deps()", please use parentheses to remove the ambiguity or change the variable name
  mix.exs:7

Compiling 2 files (.ex)
Generated issues app
warning: this check/guard will always yield the same result
  test/issues_test.exs:5

....

Finished in 0.05 seconds
4 tests, 0 failures

Randomized with seed 80914

在 mix.exs 裡面定義使用 library

mix.exs 增加 defp deps 的部分

defmodule Issues.Mixfile do
  use Mix.Project

  def project do
    [ app:     :issues,
      version: "0.0.1",
      deps:    deps 
    ]
  end

  # Configuration for the OTP application
  def application do
    [ applications: [:httpotion ] ]
  end

  # Returns the list of dependencies in the format:
  # { :foobar, "0.1", git: "https://github.com/elixir-lang/foobar.git" }
  defp deps do
    [
      { :httpotion,  github: "myfreeweb/httpotion" }
    ]
  end
end

修改 cli.exs

defmodule Issues.CLI do

  @default_count 4

  @moduledoc """
  Handle the command line parsing and the dispatch to
  the various functions that end up generating a
  table of the last _n_ issues in a github project
  """

  def run(argv) do
    argv
      |> parse_args
      |> process
  end

  @doc """
  `argv` can be -h or --help, which returns   `:help`.

  Otherwise it is a github user name, project name, and (optionally)
  the number of entries to format

  Return a tuple of `{ user, project, count }`, or `nil` if help was given.
  """
  def parse_args(argv) do
    parse = OptionParser.parse(argv, switches: [ help: :boolean],
                                     aliases:  [ h:    :help   ])
    case  parse  do

    { [ help: true ], _,           _ } -> :help
    { _, [ user, project, count ], _ } -> { user, project, String.to_integer(count) }
    { _, [ user, project ],        _ } -> { user, project, @default_count }
    _                                  -> :help
    end
  end

  def process(:help) do
    IO.puts """
    usage:  issues <user> <project> [ count | #{@default_count} ]
    """
    System.halt(0)
  end

  def process({user, project, count}) do
    Issues.GithubIssues.fetch(user, project)
  end
end

增加 lib/issues/github_issues.ex

defmodule Issues.GithubIssues do
  @user_agent  [ {"User-agent", "Elixir dave@pragprog.com"} ]

  def fetch(user, project) do
    issues_url(user, project)
    |> HTTPoison.get(@user_agent)
    |> handle_response
  end

  def issues_url(user, project) do
    "https://api.github.com/repos/#{user}/#{project}/issues"
  end

  def handle_response({ :ok, %{status_code: 200, body: body}}) do
    { :ok,    body }
  end

  def handle_response({ _,   %{status_code: _,   body: body}}) do
    { :error, body }
  end
end

增加 jsonex library

  defp deps do
    [
      {:httpotion, github: "myfreeweb/httpotion"          },
      {:jsonex,    "2.0",   github: "marcelog/jsonex", tag: "2.0"  }
    ]
  end

修改 github_issues.ex

defmodule Issues.GithubIssues do

  @user_agent  [ {"User-agent", "Elixir dave@pragprog.com"} ]

  def fetch(user, project) do
    issues_url(user, project)
    |> HTTPoison.get(@user_agent)
    |> handle_response
  end

  def handle_response({:ok, %{status_code: 200, body: body}}) do
    { :ok, Poison.Parser.parse!(body) }
  end

  def handle_response({_, %{status_code: _, body: body}}) do
    { :error, Poison.Parser.parse!(body) }
  end

  # use a module attribute to fetch the value at compile time
  @github_url Application.get_env(:issues, :github_url)

  def issues_url(user, project) do
    "#{@github_url}/repos/#{user}/#{project}/issues"
  end
end

cli_test.exs

defmodule CliTest do
  use ExUnit.Case

  import Issues.CLI, only: [ parse_args: 1,
                             sort_into_ascending_order: 1 ]

  test ":help returned by option parsing with -h and --help options" do
    assert parse_args(["-h",     "anything"]) == :help
    assert parse_args(["--help", "anything"]) == :help
  end

  test "three values returned if three given" do
    assert parse_args(["user", "project", "99"]) == { "user", "project", 99 }
  end

  test "count is defaulted if two values given" do
    assert parse_args(["user", "project"]) == { "user", "project", 4 }
  end

  test "sort ascending orders the correct way" do
    result = sort_into_ascending_order(fake_created_at_list(["c", "a", "b"]))
    issues = for issue <- result, do: Map.get(issue, "created_at")
    assert issues == ~w{a b c}
  end

  defp fake_created_at_list(values) do
    for value <- values,
    do: %{"created_at" => value, "other_data" => "xxx"}
  end
end

cli.ex

defmodule Issues.CLI do

  @default_count 4

  @moduledoc """
  Handle the command line parsing and the dispatch to
  the various functions that end up generating a
  table of the last _n_ issues in a github project
  """

  def run(argv) do
    argv
      |> parse_args
      |> process
  end

  @doc """
  `argv` can be -h or --help, which returns   `:help`.

  Otherwise it is a github user name, project name, and (optionally)
  the number of entries to format

  Return a tuple of `{ user, project, count }`, or `nil` if help was given.
  """

  def parse_args(argv) do
    parse = OptionParser.parse(argv, switches: [ help: :boolean],
                                     aliases:  [ h:    :help   ])
    case  parse  do

    { [ help: true ], _,           _ } -> :help
    { _, [ user, project, count ], _ } -> { user, project, String.to_integer(count) }
    { _, [ user, project ],        _ } -> { user, project, @default_count }
    _                                  -> :help
    end
  end

  def process(:help) do
    IO.puts """
    usage:  issues <user> <project> [ count | #{@default_count} ]
    """
    System.halt(0)
  end

  def process({user, project, count}) do
    Issues.GithubIssues.fetch(user, project)
      |> decode_response
      |> convert_to_list_of_hashdicts
      |> sort_into_ascending_order
  end

  def decode_response({:ok, body}), do: Jsonex.decode(body)
  def decode_response({:error, msg}) do
    error = Jsonex.decode(msg)["message"]
    IO.puts "Error fetching from Github: #{error}"
    System.halt(2)
  end

  def convert_to_list_of_hashdicts(list) do
    list |> Enum.map(&HashDict.new/1)
  end

  def sort_into_ascending_order(list_of_issues) do
    Enum.sort list_of_issues,
              fn i1, i2 -> i1["created_at"] <= i2["created_at"] end
  end

end

編譯時會出現 jsonex 錯誤

could not compile dependency :jsonex, "mix compile" failed. You can recompile this dependency with "mix deps.compile jsonex", update it with "mix deps.update jsonex" or clean it with "mix deps.clean jsonex"
==> issues
** (Mix) Expected :version to be a SemVer version, got: "2.0"

ref: Heroku compile issue (Elixir Buildpaack) ** (Mix) Expected :version to be a SemVer version

必須修改 Jsonex 的 mix.exs

defmodule Jsonex.Mixfile do
  use Mix.Project

  def project do
    [ app: :jsonex,
      version: "2.0.0",
      deps: deps ]
  end

同時要修改 issues 的 mix.exs

  defp deps do
    [
      {:httpotion,         github: "myfreeweb/httpotion"          },
      {:jsonex,    "2.0.0",  github: "marcelog/jsonex", tag: "2.0"  }
    ]
  end


ref: Building an Elixir CLI application

產生新的 project

$ mix new elixir_calc
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/elixir_calc.ex
* creating test
* creating test/test_helper.exs
* creating test/elixir_calc_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd elixir_calc
    mix test

Run "mix help" for more commands.

修改 mix.exs

:ex_doc 及 :earmark 是用來產生 docs 的 library

defmodule ElixirCalc.Mixfile do
  use Mix.Project

  def project do
    [
      app: :elixir_calc,
      version: "0.1.0",
      elixir: "~> 1.5",
      start_permanent: Mix.env == :prod,
      build_embedded: Mix.env == :prod,
      escript: [main_module: ElixirCalc],
      deps: deps()
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},

      {:ex_doc, "~> 0.12"},
      {:earmark, "~> 1.0", override: true}
    ]
  end
end

elixir_cals.ex: 處理 command line args

加上了 Logger.info,前面必須要先 require Logger @moduledoc 是 docs

defmodule ElixirCalc do
  require Logger

  defmodule Parser do
    def parse(args) do
      {options, args, _} = OptionParser.parse(args)
      {options, args}
    end

    def parse_args({[fib: x], _}) do
      Logger.info "arg fib= #{x} "
      IO.puts x |> String.to_integer |> ElixirCalc.Calculator.fib
    end
  end

  @moduledoc """
  main function USAGE: ./elixir_calc --fib num
  """

  def main(args) do
    args
    |> Parser.parse
    |> Parser.parse_args
  end
end

config/config.exs: 要加上 :logger 的設定值

use Mix.Config
config :logger, compile_time_purge_level: :info

lib/calculator/calculator.ex: fib 的主程式

defmodule ElixirCalc.Calculator do
  def fib(0) do 0 end
  def fib(1) do 1 end
  def fib(n) do
    fib(n - 1) + fib(n - 2)
  end
end

test/elixircalctest.exs

defmodule ElixirCalcTest do
  use ExUnit.Case
  doctest ElixirCalc

  test "fibonacci of 1 is 1" do
    assert ElixirCalc.Calculator.fib(1) == 1
  end

  test "fibonacci of 2 is 1" do
    assert ElixirCalc.Calculator.fib(2) == 1
  end

  test "fibonacci of 10 is 55" do
    assert ElixirCalc.Calculator.fib(10) == 55
  end
end
## 取得 deps libraries
$ mix deps.get
Running dependency resolution...
Dependency resolution completed:
  earmark 1.2.3
  ex_doc 0.16.3
* Getting ex_doc (Hex package)
  Checking package (https://repo.hex.pm/tarballs/ex_doc-0.16.3.tar)
  Using locally cached package
* Getting earmark (Hex package)
  Checking package (https://repo.hex.pm/tarballs/earmark-1.2.3.tar)
  Using locally cached package

## 編譯 deps
$ mix deps.compile
==> earmark
Compiling 3 files (.erl)
Compiling 24 files (.ex)
Generated earmark app
==> ex_doc
Compiling 15 files (.ex)
Generated ex_doc app

## 單元測試
$ mix test
==> earmark
Compiling 3 files (.erl)
Compiling 24 files (.ex)
Generated earmark app
==> ex_doc
Compiling 15 files (.ex)
Generated ex_doc app
==> elixir_calc
Compiling 2 files (.ex)
Generated elixir_calc app
...

Finished in 0.04 seconds
3 tests, 0 failures

Randomized with seed 704861

直接在 shell 測試

$ iex -S mix

iex(1)> ElixirCalc.main(["--fib", "10"])

22:51:00.013 [info]  arg fib= 10
55
:ok

產生獨立的執行檔及文件

## 產生獨立執行的 binary 執行檔
$ mix escript.build
Compiling 2 files (.ex)
Generated elixir_calc app
Generated escript elixir_calc with MIX_ENV=dev

## 執行 elixir_calc
$ ./elixir_calc --fib 10
55

## 產生文件
$ mix docs
Docs successfully generated.
View them at "doc/index.html".

mix 的一些指令

  • mix xref unreachable

    列出沒有被呼叫的 functions

  • mix xref warnings

    列出跟 dependencies 有關的 warnings(ex: 呼叫unknown functions)

  • mix xref callers Mod | Mod.func | Mod.func/arity

    列出呼叫 module/function 的 callers

    $ mix xref callers Logger
    lib/elixir_calc.ex:11: Logger.bare_log/3
    lib/elixir_calc.ex:11: Logger.info/1
  • mix xref graph

    列印 application dependency tree

    $ mix xref graph
    lib/calculator/calulator.ex
    lib/elixir_calc.ex
    └── lib/calculator/calulator.ex
    mix xref graph --format dot
    dot -Grankdir=LR -Epenwidth=2 -Ecolor=#a0a0a0 -Tpng xref_graph.dot -o xref_graph.png

server monitor

iex> :observer.start()

References

Programming Elixir

2018/04/16

Elixir4 StringsAndBinaries

String Literals

elixir 有兩種 string: single-quoted, double-quoted

  • strings 是使用 UTF-8 encoding
  • escape characters:

    \a BEL (0x07) \b BS (0x08) \d DEL (0x7f)
    \e ESC (0x1b) \f FF (0x0c) \n NL (0x0a)
    \r CR (0x0d) \s SP (0x20) \t TAB (0x09)
    \v VT (0x0b) \uhhh 1–6 hex digits \xhh 2 hex digits
  • 使用 #{...} 語法,處理 string interpolaton

    iex(1)> name="You"
    "You"
    iex(2)> "Hello, #{String.capitalize name}!"
    "Hello, You!"
  • 支援 heredocs """ ... """,會自動去掉每一行文字最前面的 tab/space

    iex(11)> IO.write """
    ...(11)>         my
    ...(11)>         string
    ...(11)>         """
    my
    string
    :ok
    
    iex(14)> IO.write "
    ...(14)>         my
    ...(14)>         string
    ...(14)>         "
    
            my
            string
            :ok

sigil

以下為 sigil types

~C 沒有 escaping or interpolation
~c 以 '' string 的方式 escaped and interpolated
~D 日期格式 yyyy-mm-dd
~N 原始的 DateTime 格式 yyyy-mm-dd hh:mm:ss[.ddd]
~R 沒有 escaping or interpolation 的 regular expression
~r 有 escaped and interpolated 的 regular expression
~S 沒有 escaping or interpolation 的 string
~s 以 "" string 的方式  escaped and interpolated
~T 時間格式 hh:mm:ss[.dddd]
~W whitespace-delimited words 沒有 no escaping or interpolation
~w whitespace-delimited words 有 escaping and interpolation
iex(1)> ~C[1\n2#{1+2}]
'1\\n2\#{1+2}'
iex(2)> ~c"1\n2#{1+2}"
'1\n23'
iex(3)> ~S[1\n2#{1+2}]
"1\\n2\#{1+2}"
iex(4)> ~s/1\n2#{1+2}/
"1\n23"
iex(5)> ~W[the c#{'a'}t sat on the mat]
["the", "c\#{'a'}t", "sat", "on", "the", "mat"]
iex(6)> ~w[the c#{'a'}t sat on the mat]
["the", "cat", "sat", "on", "the", "mat"]
iex(7)> ~D<1999-12-31>
~D[1999-12-31]
iex(8)> ~T[12:34:56]
~T[12:34:56]
iex(9)> ~N{1999-12-31 23:59:59}
~N[1999-12-31 23:59:59]

~W ~w 有增加 a, c, s 的選項,分別傳回 atoms, list, string of chars

iex(11)> ~w[the c#{'a'}t sat on the mat]a
[:the, :cat, :sat, :on, :the, :mat]
iex(12)> ~w[the c#{'a'}t sat on the mat]c
['the', 'cat', 'sat', 'on', 'the', 'mat']
iex(13)> ~w[the c#{'a'}t sat on the mat]s
["the", "cat", "sat", "on", "the", "mat"]

也可使用 """

iex(15)> ~w"""
...(15)> test
...(15)> 111
...(15)> """
["test", "111"]
iex(16)> ~r"""
...(16)> hello
...(16)> """i
~r/hello\n/i

single-quoted strings: lists of character codes

single-quoted strings 是 list of integer values,每個值都代表 string 的 codepoint。

'string' 如果裡面儲存的資料是可以列印的,會直接列印該文字,但其實內部是儲存為 int list

iex(1)> str = 'wombat'
'wombat'
iex(2)> is_list str
true
iex(3)> length str
6
iex(4)> Enum.reverse str
'tabmow'
iex(5)> [ 67, 65, 84 ]
'CAT'
iex(6)> :io.format "~w~n", [ str ]
[119,111,109,98,97,116]
:ok
iex(7)> List.to_tuple str
{119, 111, 109, 98, 97, 116}
iex(8)> str ++ [0]
[119, 111, 109, 98, 97, 116, 0]
iex(21)> '∂x/∂y'
[8706, 120, 47, 8706, 121]
iex(22)> 'pole' ++ 'vault'
'polevault'
iex(23)> 'pole' -- 'vault'
'poe'
iex(24)> List.zip [ 'abc', '123' ]
[{97, 49}, {98, 50}, {99, 51}]
iex(25)> [ head | tail ] = 'cat'
'cat'
iex(26)> head
99
iex(27)> tail
'at'
iex(28)> [ head | tail ]
'cat'

defmodule Parse do

  def number([ ?- | tail ]), do: _number_digits(tail, 0) * -1
  def number([ ?+ | tail ]), do: _number_digits(tail, 0)
  def number(str),           do: _number_digits(str,  0)

  defp _number_digits([], value), do: value
  defp _number_digits([ digit | tail ], value)
  when digit in '0123456789' do
    _number_digits(tail, value*10 + digit - ?0)
  end
  defp _number_digits([ non_digit | _ ], _) do
    raise "Invalid digit '#{[non_digit]}'"
  end
end
iex(1)> Parse.number('123')
123
iex(2)> Parse.number('+123')
123
iex(3)> Parse.number('-123')
-123
iex(4)> Parse.number('+a')
** (RuntimeError) Invalid digit 'a'
    parse.exs:13: Parse._number_digits/2

Binaries

<< term,… >>
iex(1)> b = << 1, 2, 3 >>
<<1, 2, 3>>
iex(2)> byte_size b
3
iex(3)> bit_size b
24

iex(5)> b = << 1::size(2), 1::size(3) >>
<<9::size(5)>>
iex(6)> byte_size b
1
iex(7)> bit_size b
5

可儲存 integers, floats, binaries

iex(13)> int = << 1 >>
<<1>>
iex(14)> float = << 2.5 :: float >>
<<64, 4, 0, 0, 0, 0, 0, 0>>
iex(15)> mix = << int :: binary, float :: binary >>
<<1, 64, 4, 0, 0, 0, 0, 0, 0>>

IEEE 754 float has a sign bit, 11 bits of exponent, and 52 bits of mantissa,可直接用 pattern matching 的方式拆解 float

iex(22)> << sign::size(1), exp::size(11), mantissa::size(52) >> = << 3.14159::float >>
<<64, 9, 33, 249, 240, 27, 134, 110>>
iex(23)> (1 + mantissa / :math.pow(2, 52)) * :math.pow(2, exp-1023)
3.14159

Double-Quoted Strings 就等同於 Binaries,但字串長度並不等於 bytes size,因為 string 是使用 UTF-8 encoding

iex(1)> dqs = "∂x/∂y"
"∂x/∂y"
iex(2)> String.length dqs
5
iex(3)> byte_size dqs
9
iex(4)> String.at(dqs, 0)
"∂"
iex(5)> String.codepoints(dqs)
["∂", "x", "/", "∂", "y"]
iex(6)> String.split(dqs, "/")
["∂x", "∂y"]

使用 string(binary) 的 elixir library

# at(str, offset)
#  在某個位置的 char

iex> String.at("∂og", 0)
"∂"
iex> String.at("∂og", -1)
"g"

# capitalize(str)
#  轉成小寫, 首字元大寫
iex> String.capitalize "école"
"École"
iex> String.capitalize "ÎÎÎÎÎ"
"Îîîîî"


# codepoints(str)
#  字串的 codepoints
iex> String.codepoints("José's ∂øg")
["J", "o", "s", "é", "'", "s", " ", "∂", "ø", "g"]

# downcase(str)
#  轉小寫
iex> String.downcase "ØRSteD"
"ørsted"

# duplicate(str, n)
#  重複 n 次
iex> String.duplicate "Ho! ", 3
"Ho! Ho! Ho! "

# ends_with?(str, suffix | [ suffixes ])
#  是否以某一個 suffixes 結束
iex> String.ends_with? "string", ["elix", "stri", "ring"]
true


# first(str)
#  第一個 char
iex> String.first "∂og"
"∂"


# graphemes(str)
#  與 codepoints 不同
iex> String.codepoints "noe\u0308l"
["n", "o", "e", "¨", "l"]
iex> String.graphemes "noe\u0308l"
["n", "o", "ë", "l"]

# jaro_distance
#  以 0~1 float 代表兩個 string 的差異
iex> String.jaro_distance("jonathan", "jonathon")
0.9166666666666666
iex> String.jaro_distance("josé", "john")
0.6666666666666666


# last(str)
#  最後一個字元
iex> String.last "∂og"
"g"

# length(str)
#  字串長度, 用字元計算
Returns the number of graphemes in str.
iex> String.length "∂x/∂y"
5

# myers_difference
#  由一個 string 轉換成另一個的過程
iex> String.myers_difference("banana", "panama")
[del: "b", ins: "p", eq: "ana", del: "n", ins: "m", eq: "a"]


# next_codepoint(str)
#  分割第一個字元跟剩下的字串

defmodule MyString do
    def each(str, func), do: _each(String.next_codepoint(str), func)
    defp _each({codepoint, rest}, func) do
        func.(codepoint)
            _each(String.next_codepoint(rest), func)
    end
    defp _each(nil, _), do: []
end
MyString.each "∂og", fn c -> IO.puts c end
# next_grapheme(str)
#  跟 next_codepoint 一樣,但 return graphemes,以 :no_grapheme 結束


# pad_leading(str, new_length, padding \\ 32)
#  以 str 結束,至少 new_length 這麼長的字串,字串前面的以 padding 補上
iex> String.pad_leading("cat", 5, ?>)
">>cat"


# pad_trailing(str, new_length, padding \\ " ")
#  同 pad_leading 但 padding 放在字串的後面
iex> String.pad_trailing("cat", 5)
"cat "


# printable?(str)
#  是否為 printable chars
iex> String.printable? "José"
true
iex> String.printable? "\x{0000} a null"
false


# replace(str, pattern, replacement, options \\ [global: true, insert_replaced: nil])
#  替換字串, :global 代表每一次都替換, :insert_replaced 代表要 insert 在後面 offset 個字元
iex> String.replace "the cat on the mat", "at", "AT"
"the cAT on the mAT"
iex> String.replace "the cat on the mat", "at", "AT", global: false
"the cAT on the mat"
iex> String.replace "the cat on the mat", "at", "AT", insert_replaced: 0
"the catAT on the matAT"
iex> String.replace "the cat on the mat", "at", "AT", insert_replaced: [0,2]
"the catATat on the matATat"


# reverse(str)
iex> String.reverse "pupils"
"slipup"
iex> String.reverse "∑ƒ÷∂"
"∂÷ƒ∑"

# slice(str, offset, len)
#  由 offset 位置開始取 len 個 chars
iex> String.slice "the cat on the mat", 4, 3
"cat"
iex> String.slice "the cat on the mat", -3, 3
"mat"

# split(str, pattern \\ nil, options \\ [global: true])
#  以 pattern 為分割點(預設為 space),切割 string
iex> String.split " the cat on the mat "
["the", "cat", "on", "the", "mat"]
iex> String.split "the cat on the mat", "t"
["", "he ca", " on ", "he ma", ""]
iex> String.split "the cat on the mat", ~r{[ae]}
["th", " c", "t on th", " m", "t"]
iex> String.split "the cat on the mat", ~r{[ae]}, parts: 2
["th", " cat on the mat"]


# starts_with?(str, prefix | [ prefixes ])
#  是否以 prefix 開頭
iex> String.starts_with? "string", ["elix", "stri", "ring"]
true


# trim(str)
#  去掉前後的 whitespaces
iex> String.trim "\t Hello \r\n"
"Hello

# trim(str, character)
#  去掉前後的 character
iex> String.trim "!!!SALE!!!", "!"
"SALE"

# trim_leading(str)
iex> String.trim_leading "\t\f Hello\t\n"
"Hello\t\n"

# trim_leading(str, character)
iex> String.trim_leading "!!!SALE!!!", "!"
"SALE!!!"

# trim_trailing(str)
iex> String.trim_trailing(" line \r\n")
" line"

# trim_trailing(str, character)
iex> String.trim_trailing "!!!SALE!!!", "!"
"!!!SALE"

# upcase(str)
iex> String.upcase "José Ørstüd"
"JOSÉ ØRSTÜD"

# valid?(str)
#  判斷是否為單一 char
iex> String.valid? "∂"
true
iex> String.valid? "∂og"
false

Binaries and Pattern Matching

可用在 Binary 的 type: binary, bits, bitstring, bytes, float, integer, utf8, utf16, and utf32

  • size(n): size in bits
  • signed/unsigned
  • endianness: big/little/native
<< length::unsigned-integer-size(12), flags::bitstring-size(4) >> = data

以 Binary 方式處理 String

defmodule Utf8 do
  def each(str, func) when is_binary(str), do: _each(str, func)

  # 用 binary pattern matching 以一個 utf8 字元為 head
  defp _each(<< head :: utf8, tail :: binary >>, func) do
    func.(head)
    _each(tail, func)
  end

  defp _each(<<>>, _func), do: []
end

Utf8.each "∂og", fn char -> IO.puts char end

Control Flow

elixir 有提供這些語法,但應該盡量使用 pattern matching 的方式處理

if, unless

iex(1)> if 1 == 1, do: "true part", else: "false part"
"true part"
iex(2)> if 2 == 1, do: "true part", else: "false part"
"false part"
iex(4)> if 1==1 do
...(4)>   "true"
...(4)> else
...(4)>   "false"
...(4)> end
"true"
iex(6)> unless 1 == 1, do: "error", else: "OK"
"OK"
iex(8)> unless 1 == 2, do: "OK", else: "error"
"OK"
iex(10)> unless 1 == 2 do
...(10)>   "OK"
...(10)> else
...(10)>   "error"
...(10)> end
"OK"

cond

FizzBuzz: 如果是 3 的倍數就印 Fizz,如果是 5 的倍數就印 Buzz,如果同時是 3 和 5 的倍數就印 FizzBuzz

defmodule FizzBuzz do

  def upto(n) when n > 0, do: _upto(1, n, [])

  defp _upto(_current, 0, result),  do: Enum.reverse result

  defp _upto(current, left, result) do
    next_answer =
      cond do
        rem(current, 3) == 0 and rem(current, 5) == 0 ->
          "FizzBuzz"
        rem(current, 3) == 0 ->
          "Fizz"
        rem(current, 5) == 0 ->
          "Buzz"
        true ->
          current
      end
    _upto(current+1, left-1, [ next_answer | result ])
  end
end

因為處理順序的關係,必須要在最後 Enum.reverse result

iex(1)> FizzBuzz.upto(10)
[1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz"]

所以就反過來,從 n 開始做

defmodule FizzBuzz do

  def upto(n) when n > 0, do: _downto(n, [])

  defp _downto(0, result),  do: result

  defp _downto(current, result) do
    next_answer =
      cond do
        rem(current, 3) == 0 and rem(current, 5) == 0 ->
          "FizzBuzz"
        rem(current, 3) == 0 ->
          "Fizz"
        rem(current, 5) == 0 ->
          "Buzz"
        true ->
          current
      end
    _downto(current-1, [ next_answer | result ])
  end
end

改用 Enum.map 重複呼叫 fizzbuzz

defmodule FizzBuzz do

  def upto(n) when n > 0 do
    1..n |> Enum.map(&fizzbuzz/1)
  end

  defp fizzbuzz(n) do
    cond do
      rem(n, 3) == 0 and rem(n, 5) == 0 ->
        "FizzBuzz"
      rem(n, 3) == 0 ->
        "Fizz"
      rem(n, 5) == 0 ->
        "Buzz"
      true ->
        n
    end
  end
end

最好的方式是用 pattern matching 的方式

defmodule FizzBuzz do

  def upto(n) when n > 0 do
    1..n |> Enum.map(&fizzbuzz/1)
  end

  defp fizzbuzz(n) when rem(n, 3) == 0 and rem(n, 5) == 0, do: "FizzBuzz"
  defp fizzbuzz(n) when rem(n, 3) == 0, do: "Fizz"
  defp fizzbuzz(n) when rem(n, 5) == 0, do: "Buzz"
  defp fizzbuzz(n), do: n

end

case

File.open 會遇到兩種 return

case File.open("case.ex")  do

{ :ok, file } ->
  IO.puts "First line: #{IO.read(file, :line)}"

{ :error, reason } ->
  IO.puts "Failed to open file: #{reason}"

end

加上 guard clause

defrecord Person, name: "", age: 0

defmodule Bouncer do

  dave = Person.new name: "You", age: 27

  case dave do

    record = Person[age: age] when is_number(age) and age >= 21 ->
      IO.puts "You are cleared to enter the Foo Bar, #{record.name}"

    _ ->
      IO.puts "Sorry, no admission"

  end
end

Expcetions

iex(1)> raise "Give Up"
** (RuntimeError) Give Up

iex(1)> raise RuntimeError
** (RuntimeError) runtime error

iex(1)> raise RuntimeError, message: "override message"
** (RuntimeError) override message

錯誤時 raise Exception

case File.open("config_file") do
{:ok, file} ->
  process(file)
{:error, message} ->
  raise "Failed to open config file: #{message}"
end

或是由 pattern matching 的方式處理

{ :ok, file } = File.open("config_file")
process(file)

References

Programming Elixir

2018/04/09

Elixir 3 Lists_Maps

Lists


Heads and Tails

[] 為 empty list

[ 3 | [] ] 就等於 [3]

[ 1 | [ 2 | [ 3 | [] ] ] ] 就等於 [1, 2, 3]

iex(2)> [ head | tail ] = [ 1, 2, 3 ]
[1, 2, 3]
iex(3)> head
1
iex(4)> tail
[2, 3]

利用 Head Tail 不斷 recursive 呼叫 func,沒有使用的參數 _head 前面加上底線,就不會出現編譯的 warning。

defmodule MyList do
  def len([]), do: 0
  def len([_head|tail]), do: 1 + len(tail)
end

運算過程為

len([11,12,13,14,15])
= 1 + len([12,13,14,15])
= 1 + 1 + len([13,14,15])
= 1 + 1 + 1 + len([14,15])
= 1 + 1 + 1 + 1 + len([15])
= 1 + 1 + 1 + 1 + 1 + len([])
= 1 + 1 + 1 + 1 + 1 + 0
= 5

Map function for list

defmodule MyList do
  def map([], _func),             do: []
  def map([ head | tail ], func), do: [ func.(head) | map(tail, func) ]
end

使用 map

iex(1)> MyList.map [1,2,3,4], fn (n) -> n*n end
[1, 4, 9, 16]

iex(2)> MyList.map [1,2,3,4], fn (n) -> n+1 end
[2, 3, 4, 5]
iex(3)> MyList.map [1,2,3,4], &(&1 + 1)
[2, 3, 4, 5]

在 recursion 過程中,持續保留結果

  • sum([]) -> 0
  • sum([ head |tail ]) -> «total» + sum(tail)
defmodule MyList do
  def sum([], total),              do: total
  def sum([ head | tail ], total), do: sum(tail, head+total)
end
MyList.sum([1,2,3,4,5], 0)

剛剛的寫法,呼叫時要先傳入初始化的 total,也就是 0,改寫為以下做法,隱藏實際的 _sum 演算法,另外定義一個新的 public function,只對外開放可使用這個 function

defmodule MyList do

  def sum(list), do: _sum(list, 0)

  # private methods
  defp _sum([], total),              do: total
  defp _sum([ head | tail ], total), do: _sum(tail, head+total)
end

list 的 reduce function 定義

reduce(collection, initial_value, fun)
  • reduce([], value, _) -> value
  • reduce([ head |tail ], value, fun) -> reduce(tail, fun.(head, value), fun)
defmodule MyList do
  def reduce([], value, _) do
    value
  end
  def reduce([head | tail], value, fun) do
    reduce(tail, fun.(head, value), fun)
  end
end

使用 reduce.exs

iex(1)> MyList.reduce([1,2,3,4,5], 0, &(&1 + &2))
15
iex(2)> MyList.reduce([1,2,3,4,5], 1, &(&1 * &2))
120

More complex List Patterns

defmodule Swapper do

  def swap([]), do: []
  def swap([ a, b | tail ]), do: [ b, a | swap(tail) ]
  def swap([_]), do: raise "Can't swap a list with an odd number of elements"

end

執行結果

iex(1)> Swapper.swap [1,2,3,4,5,6]
[2, 1, 4, 3, 6, 5]

Lists of Lists

defmodule WeatherHistory do

  def test_data do
    [
     [1366225622, 26, 15, 0.125],
     [1366225622, 27, 15, 0.45],
     [1366225622, 28, 21, 0.25],
     [1366229222, 26, 19, 0.081],
     [1366229222, 27, 17, 0.468],
     [1366229222, 28, 15, 0.60],
     [1366232822, 26, 22, 0.095],
     [1366232822, 27, 21, 0.05],
     [1366232822, 28, 24, 0.03],
     [1366236422, 26, 17, 0.025]
    ]
  end


  # standard recurse until the list is empty stanza

  def for_location_27([]), do: []
  def for_location_27([ [time, 27, temp, rain ] | tail]) do
    [ [time, 27, temp, rain] | for_location_27(tail) ]
  end
  def for_location_27([ _ | tail]), do: for_location_27(tail)

end
iex(1)> import WeatherHistory
WeatherHistory
iex(2)> for_location_27(test_data)
[[1366225622, 27, 15, 0.45], [1366229222, 27, 17, 0.468],
 [1366232822, 27, 21, 0.05]]

改進 weather2.exs

defmodule WeatherHistory do

  def test_data do
    [
     [1366225622, 26, 15, 0.125],
     [1366225622, 27, 15, 0.45],
     [1366225622, 28, 21, 0.25],
     [1366229222, 26, 19, 0.081],
     [1366229222, 27, 17, 0.468],
     [1366229222, 28, 15, 0.60],
     [1366232822, 26, 22, 0.095],
     [1366232822, 27, 21, 0.05],
     [1366232822, 28, 24, 0.03],
     [1366236422, 26, 17, 0.025]
    ]
  end

  def for_location([], _target_loc), do: []

  def for_location([ [time, target_loc, temp, rain ] | tail], target_loc) do
    [ [time, target_loc, temp, rain] | for_location(tail, target_loc) ]
  end

  def for_location([ _ | tail], target_loc), do: for_location(tail, target_loc)

end

weather3.exs

defmodule WeatherHistory do

  def test_data do
    [
     [1366225622, 26, 15, 0.125],
     [1366225622, 27, 15, 0.45],
     [1366225622, 28, 21, 0.25],
     [1366229222, 26, 19, 0.081],
     [1366229222, 27, 17, 0.468],
     [1366229222, 28, 15, 0.60],
     [1366232822, 26, 22, 0.095],
     [1366232822, 27, 21, 0.05],
     [1366232822, 28, 24, 0.03],
     [1366236422, 26, 17, 0.025]
    ]
  end

  def for_location([], target_loc), do: []

  def for_location([ head = [_, target_loc, _, _ ] | tail], target_loc) do
    [ head | for_location(tail, target_loc) ]
  end

  def for_location([ _ | tail], target_loc), do: for_location(tail, target_loc)

end

functions in List Module

#
# Concatenate lists
#
iex> [1,2,3] ++ [4,5,6]
[1, 2, 3, 4, 5, 6]
#
# Flatten
#
iex> List.flatten([[[1], 2], [[[3]]]])
[1, 2, 3]
#
# Folding (like reduce, but can choose direction)
#
iex> List.foldl([1,2,3], "", fn value, acc -> "#{value}(#{acc})" end)
"3(2(1()))"
iex> List.foldr([1,2,3], "", fn value, acc -> "#{value}(#{acc})" end)
"1(2(3()))"
#
# Updating in the middle (not a cheap operation)
#
iex> list = [ 1, 2, 3 ]
[ 1, 2, 3 ]
iex> List.replace_at(list, 2, "buckle my shoe")
[1, 2, "buckle my shoe"]
#
# Accessing tuples within lists
#
iex> kw = [{:name, "Dave"}, {:likes, "Programming"}, {:where, "Dallas", "TX"}]
[{:name, "Dave"}, {:likes, "Programming"}, {:where, "Dallas", "TX"}]
iex> List.keyfind(kw, "Dallas", 1)
{:where, "Dallas", "TX"}
iex> List.keyfind(kw, "TX", 2)
{:where, "Dallas", "TX"}
iex> List.keyfind(kw, "TX", 1)
nil
iex> List.keyfind(kw, "TX", 1, "No city called TX")
"No city called TX"
iex> kw = List.keydelete(kw, "TX", 2)
[name: "Dave", likes: "Programming"]
iex> kw = List.keyreplace(kw, :name, 0, {:first_name, "Dave"})
[first_name: "Dave", likes: "Programming"]

Dictionary: Map, Keyword Lists, Sets, Structs

dictionary: a data type that associates keys with values


選用 Maps 或是 Keyword Lists?

依序考慮下列問題

  1. 需不需要使用 pattern-matching for contents? 例如 matching 有 :name 為 key 的 dictionary。 如果有需要就使用 map

  2. 需不需要用同樣的 key 儲存不同的 value? 如果有就使用 Keyword module

  3. 儲存的元素要不要保留順序? 如果有就使用 Keyword module

  4. 最後,就直接用 map

Keyword Lists

defmodule Canvas do

  @defaults [ fg: "black", bg: "white", font: "Merriweather" ]

  def draw_text(text, options \\ []) do
    options = Keyword.merge(@defaults, options)
    IO.puts "Drawing text #{inspect(text)}"
    IO.puts "Foreground:  #{options[:fg]}"
    IO.puts "Background:  #{Keyword.get(options, :bg)}"
    IO.puts "Font:        #{Keyword.get(options, :font)}"
    IO.puts "Pattern:     #{Keyword.get(options, :pattern, "solid")}"
    IO.puts "Style:       #{inspect Keyword.get_values(options, :style)}"
  end

end

Canvas.draw_text("hello", fg: "red", style: "italic", style: "bold")

以 key 使用 map 裡面的value: kwlist[key]

Keyword List 可使用 KeywordEnum modules 的所有 functions

Maps

iex(1)> map = %{ name: "You", likes: "Programming", where: "Home" }
%{likes: "Programming", name: "You", where: "Home"}
iex(2)> Map.keys map
[:likes, :name, :where]
iex(3)> Map.values map
["Programming", "You", "Home"]
iex(4)> map[:name]
"You"
iex(5)> map.name
"You"
iex(6)> map1 = Map.drop map, [:where, :likes]
%{name: "You"}
iex(7)> map2 = Map.put map, :also_likes, "Ruby"
%{also_likes: "Ruby", likes: "Programming", name: "You", where: "Home"}
iex(8)> Map.keys map2
[:also_likes, :likes, :name, :where]
iex(9)> Map.has_key? map1, :where
false
iex(10)> { value, updated_map } = Map.pop map2, :also_likes
{"Ruby", %{likes: "Programming", name: "You", where: "Home"}}
iex(11)> Map.equal? map, updated_map
true

Pattern Matching and Updating Maps

利用 pattern matching,在 match 同時, binding 變數

iex(15)> person = %{ name: "You", height: 1.88 }
%{height: 1.88, name: "You"}

# 是否有存在key  :name
iex(16)> %{ name: a_name } = person
%{height: 1.88, name: "You"}
iex(17)> a_name
"You"
iex(18)> %{ name: _, height: _ } = person
%{height: 1.88, name: "You"}

## :name 為 "You" ?
iex(19)> %{ name: "You" } = person
%{height: 1.88, name: "You"}

## not match error
iex(20)> %{ name: _, weight: _ } = person
** (MatchError) no match of right hand side value: %{height: 1.88, name: "You"}

iterate 所有 key-value,取得符合某個條件(height > 1.5) 的 map

people = [
  %{ name: "Grumpy",    height: 1.24 },
  %{ name: "Dave",      height: 1.88 },
  %{ name: "Dopey",     height: 1.32 },
  %{ name: "Shaquille", height: 2.16 },
  %{ name: "Sneezy",    height: 1.28 }
]

IO.inspect(for person = %{ height: height } <- people, height > 1.5, do: person)
people = [
  %{ name: "Grumpy",    height: 1.24 },
  %{ name: "Dave",      height: 1.88 },
  %{ name: "Dopey",     height: 1.32 },
  %{ name: "Shaquille", height: 2.16 },
  %{ name: "Sneezy",    height: 1.28 }
]

defmodule HotelRoom do

  def book(%{name: name, height: height})
  when height > 1.9 do
    IO.puts "Need extra long bed for #{name}"
  end

  def book(%{name: name, height: height})
  when height < 1.3 do
    IO.puts "Need low shower controls for #{name}"
  end

  def book(person) do
    IO.puts "Need regular bed for #{person.name}"
  end

end

people |> Enum.each(&HotelRoom.book/1)

結果為

Need low shower controls for Grumpy
Need regular bed for Dave
Need regular bed for Dopey
Need extra long bed for Shaquille
Need low shower controls for Sneezy

不能在 pattern matching 時,同時 bind key variable

iex(1)> %{ 2 => state } = %{ 1 => :ok, 2 => :error }
%{1 => :ok, 2 => :error}
iex(2)> state
:error
iex(3)> %{ item => :ok } = %{ 1 => :ok, 2 => :error }
** (CompileError) iex:3: illegal use of variable item inside map key match, maps can only match on existing variables by using ^item
    (stdlib) lists.erl:1354: :lists.mapfoldl/3

Pattern Matching 可 match variable keys

iex(11)> data = %{ name: "You", state: "Home", likes: "Elixir" }
%{likes: "Elixir", name: "You", state: "Home"}

iex(12)> for key <- [ :name, :likes ] do
...(12)>   %{ ^key => value } = data
...(12)>   value
...(12)> end
["You", "Elixir"]

Updating Maps 最簡單的方式為

new_map = %{ old_map | key => value, … }

會先複製一份舊的 map,並將新的 key,value 替換掉

iex(16)> m = %{ a: 1, b: 2, c: 3 }
%{a: 1, b: 2, c: 3}
iex(17)> m1 = %{ m | b: "two", c: "three" }
%{a: 1, b: "two", c: "three"}
iex(18)>  m2 = %{ m1 | a: "one" }
%{a: "one", b: "two", c: "three"}

Structs

struct 就是一個 limited form of map, keys 一定要是 atoms,module 名稱就是這個 map type 的名稱

defmodule Subscriber do
  defstruct name: "", paid: false, over_18: true
end

使用 Subscriber 的方式

iex(1)> s1 = %Subscriber{}
%Subscriber{name: "", over_18: true, paid: false}
iex(2)> s2 = %Subscriber{ name: "You" }
%Subscriber{name: "You", over_18: true, paid: false}
iex(3)> s3 = %Subscriber{ name: "Mary", paid: true }
%Subscriber{name: "Mary", over_18: true, paid: true}
iex(4)>
nil
iex(5)> s3.name
"Mary"
iex(6)> %Subscriber{name: a_name} = s3
%Subscriber{name: "Mary", over_18: true, paid: true}
iex(7)> a_name
"Mary"
iex(8)>
nil
iex(9)> s4 = %Subscriber{ s3 | name: "Marie"}
%Subscriber{name: "Marie", over_18: true, paid: true}

限制一定要用 module 封裝 structs 的原因是,要在 module 裡面加上對該 structs 相關的操作 functions

iex(1)> a1 = %Attendee{name: "You", over_18: true}
%Attendee{name: "You", over_18: true, paid: false}
iex(2)> a2 = %Attendee{a1 | paid: true}
%Attendee{name: "You", over_18: true, paid: true}
iex(3)> Attendee.may_attend_after_party(a2)
true
iex(4)> Attendee.print_vip_badge(a2)
Very cheap badge for You
:ok
iex(5)> a3 = %Attendee{}
%Attendee{name: "", over_18: true, paid: false}
iex(6)> Attendee.print_vip_badge(a3)
** (RuntimeError) missing name for badge
    defstruct1.exs:21: Attendee.print_vip_badge/1

Nested Ductionary Structures

可在 value 的部分使用 dictionary


defmodule Customer do
  defstruct name: "", company: ""
end

defmodule BugReport do
  defstruct owner: %Customer{}, details: "", severity: 1
end

defmodule User do
  report = %BugReport{owner: %Customer{name: "You", company: "Company"},
                      details: "broken"}

  IO.inspect report

  report = %BugReport{ report | owner: %Customer{ report.owner | company: "PragProg" }}

  IO.inspect report

  IO.inspect update_in(report.owner.name, &("Mr. " <> &1))
end

執行結果

%BugReport{details: "broken", owner: %Customer{company: "Company", name: "You"},
 severity: 1}
%BugReport{details: "broken",
 owner: %Customer{company: "PragProg", name: "You"}, severity: 1}
%BugReport{details: "broken",
 owner: %Customer{company: "PragProg", name: "Mr. You"}, severity: 1}

Nested Accessors and Nonstructs

put_in(path, value)
put_in( data, keys, value)

update_in(path, fun)
update_in(data, keys, fun)
iex(1)> report = %{ owner: %{ name: "You", company: "Company" }, severity: 1}
%{owner: %{company: "Company", name: "You"}, severity: 1}
iex(2)> put_in(report[:owner][:company], "Com")
%{owner: %{company: "Com", name: "You"}, severity: 1}
iex(3)> update_in(report[:owner][:name], &("Mr. " <> &1))
%{owner: %{company: "Company", name: "Mr. You"}, severity: 1}

Dynamic (Runtime) Nested Accessors

get_in
put_in
update_in
get_and_update_in

ex:


nested = %{
    buttercup: %{
      actor: %{
        first: "Robin",
        last:  "Wright"
      },
      role: "princess"
    },
    westley: %{
      actor: %{
        first: "Cary",
        last:  "Ewles"     # typo!
      },
      role: "farm boy"
    }
}

IO.inspect get_in(nested, [:buttercup])
# => %{actor: %{first: "Robin", last: "Wright"}, role: "princess"}

IO.inspect get_in(nested, [:buttercup, :actor])
# => %{first: "Robin", last: "Wright"}

IO.inspect get_in(nested, [:buttercup, :actor, :first])
# => "Robin"

IO.inspect put_in(nested, [:westley, :actor, :last], "Elwes")
# => %{buttercup: %{actor: %{first: "Robin", last: "Wright"}, role: "princess"},
# =>     westley: %{actor: %{first: "Cary", last: "Elwes"}, role: "farm boy"}}

可利用 fun 當作 keys 傳入 get_in

get_in(data, keys)
authors = [
  %{ name: "José",  language: "Elixir" },
  %{ name: "Matz",  language: "Ruby" },
  %{ name: "Larry", language: "Perl" }
]

languages_with_an_r = fn (:get, collection, next_fn) ->
   for row <- collection do
     if String.contains?(row.language, "r") do
       next_fn.(row)
     end
   end
end

IO.inspect get_in(authors, [languages_with_an_r, :name]) 
#=> [ "José", nil, "Larry" ]

Access Module

提供 get 與 get_and_update_in 一些 predefined functions 當作參數

Access.all(), Access.at(1)


cast = [
  %{
    character: "Buttercup",
    actor: %{
      first: "Robin",
      last:  "Wright"
    },
    role: "princess"
  },
  %{
    character: "Westley",
    actor: %{
      first: "Cary",
      last:  "Elwes"
    },
    role: "farm boy"
  }
]

IO.inspect get_in(cast, [Access.all(), :character])
#=> ["Buttercup", "Westley"]

IO.inspect get_in(cast, [Access.at(1), :role])
#=> "farm boy"

IO.inspect get_and_update_in(cast, [Access.all(), :actor, :last],
                             fn (val) -> {val, String.upcase(val)} end)
#=> {["Wright", "Ewes"],
#    [%{actor: %{first: "Robin", last: "WRIGHT"}, character: "Buttercup",
#       role: "princess"},
#     %{actor: %{first: "Cary", last: "EWES"}, character: "Westley",
#       role: "farm boy"}]}

Access.elem 可處理 tuples

cast = [
  %{
    character: "Buttercup",
    actor:    {"Robin", "Wright"},
    role:      "princess"
  },
  %{
    character: "Westley",
    actor:    {"Carey", "Elwes"},
    role:      "farm boy"
  }
]

IO.inspect get_in(cast, [Access.all(), :actor, Access.elem(1)])
#=> ["Wright", "Elwes"]

IO.inspect get_and_update_in(cast, [Access.all(), :actor, Access.elem(1)],
                             fn (val) -> {val, String.reverse(val)} end)
#=> {["Wright", "Elwes"],
#    [%{actor: {"Robin", "thgirW"}, character: "Buttercup", role: "princess"},
#     %{actor: {"Carey", "sewlE"}, character: "Westley", role: "farm boy"}]}

Access.key Access.key! 可處理 dictionary types(maps, structs)


cast = %{
  buttercup: %{
    actor:    {"Robin", "Wright"},
    role:      "princess"
  },
  westley: %{
    actor:    {"Carey", "Elwes"},
    role:      "farm boy"
  }
}

IO.inspect get_in(cast, [Access.key(:westley), :actor, Access.elem(1)])
#=> "Elwes"

IO.inspect get_and_update_in(cast, [Access.key(:buttercup), :role],
                             fn (val) -> {val, "Queen"} end)
#=> {"princess",
#    %{buttercup: %{actor: {"Robin", "Wright"}, role: "Queen"},
#      westley: %{actor: {"Carey", "Elwes"}, role: "farm boy"}}}

Access.pop 可從 map/keyword list 移出一個 key

iex(1)> Access.pop(%{name: "Elixir", creator: "Valim"}, :name)
{"Elixir", %{creator: "Valim"}}
iex(2)> Access.pop([name: "Elixir", creator: "Valim"], :name)
{"Elixir", [creator: "Valim"]}
iex(3)> Access.pop(%{name: "Elixir", creator: "Valim"}, :year)
{nil, %{creator: "Valim", name: "Elixir"}}

Sets

Sets 是以 MapSet module 實作的

iex(1)> set1 = 1..5 |> Enum.into(MapSet.new)
#MapSet<[1, 2, 3, 4, 5]>
iex(2)> set2 = 3..8 |> Enum.into(MapSet.new)
#MapSet<[3, 4, 5, 6, 7, 8]>
iex(3)> MapSet.member? set1, 3
true
iex(4)> MapSet.union set1, set2
#MapSet<[1, 2, 3, 4, 5, 6, 7, 8]>
iex(5)> MapSet.difference set1, set2
#MapSet<[1, 2]>
iex(6)> MapSet.difference set2, set1
#MapSet<[6, 7, 8]>
iex(7)> MapSet.intersection set2, set1
#MapSet<[3, 4, 5]>

Enum and Stream

elixir 的 Enum and Stream 有一下 iteration functions,Stream 可 enumerate a collection lazily,也就是說下一個 value 只在需要時才會計算

Enum

## 將 collection 轉換為 list
iex(1)> list = Enum.to_list 1..5
[1, 2, 3, 4, 5]

## 連接兩個 collections
iex(2)> Enum.concat([1,2,3], [4,5,6])
[1, 2, 3, 4, 5, 6]
iex(3)> Enum.concat [1,2,3], 'abc'
[1, 2, 3, 97, 98, 99]

## 將 list 每個 elements 放到 function 產生新的 list
iex(4)> Enum.map(list, &(&1 * 10))
[10, 20, 30, 40, 50]
iex(5)> Enum.map(list, &String.duplicate("*", &1))
["*", "**", "***", "****", "*****"]


## 根據 position/criteria 取得 list 的 elements
iex(8)> Enum.at(10..20, 3)
13
iex(9)> Enum.at(10..20, 20)
nil
iex(10)> Enum.at(10..20, 20, :no_one_here)
:no_one_here
iex(11)> Enum.filter(list, &(&1 > 2))
[3, 4, 5]
iex(12)> require Integer
Integer
iex(13)> Enum.filter(list, &Integer.is_even/1)
[2, 4]
iex(14)> Enum.reject(list, &Integer.is_even/1)
[1, 3, 5]

## 排序/比較 elements
iex(16)> Enum.sort ["there", "was", "a", "crooked", "man"]
["a", "crooked", "man", "there", "was"]
iex(17)> Enum.sort ["there", "was", "a", "crooked", "man"], &(String.length(&1) <= String.length(&2))
["a", "was", "man", "there", "crooked"]
iex(18)>  Enum.max ["there", "was", "a", "crooked", "man"]
"was"
iex(19)> Enum.max_by ["there", "was", "a", "crooked", "man"], &String.length/1
"crooked"

## split a collection
iex(20)> Enum.take(list, 3)
[1, 2, 3]
iex(21)> Enum.take_every list, 2
[1, 3, 5]
iex(22)> Enum.take_while(list, &(&1 < 4))
[1, 2, 3]
iex(23)> Enum.split(list, 3)
{[1, 2, 3], [4, 5]}
iex(24)> Enum.split_while(list, &(&1 < 4))
{[1, 2, 3], [4, 5]}

## join a collection
iex(25)> Enum.join(list)
"12345"
iex(26)> Enum.join(list, ", ")
"1, 2, 3, 4, 5"


## predicate operations
iex(27)> Enum.all?(list, &(&1 < 4))
false
iex(28)> Enum.any?(list, &(&1 < 4))
true
iex(29)> Enum.member?(list, 4)
true
iex(30)> Enum.empty?(list)
false

## merge collections
iex(31)> Enum.zip(list, [:a, :b, :c])
[{1, :a}, {2, :b}, {3, :c}]
iex(32)> Enum.with_index(["once", "upon", "a", "time"])
[{"once", 0}, {"upon", 1}, {"a", 2}, {"time", 3}]

## Fold elements into a single value
iex(33)> Enum.reduce(1..100, &(&1+&2))
5050

iex(37)> Enum.reduce(["now", "is", "the", "time"],fn word, longest ->
...(37)> if String.length(word) > String.length(longest) do
...(37)> word
...(37)> else
...(37)> longest
...(37)> end
...(37)> end)
"time"

處理撲克牌

iex(1)> import Enum
Enum
iex(2)> deck = for rank <- '23456789TJQKA', suit <- 'CDHS', do: [suit,rank]
['C2', 'D2', 'H2', 'S2', 'C3', 'D3', 'H3', 'S3', 'C4', 'D4', 'H4', 'S4', 'C5',
 'D5', 'H5', 'S5', 'C6', 'D6', 'H6', 'S6', 'C7', 'D7', 'H7', 'S7', 'C8', 'D8',
 'H8', 'S8', 'C9', 'D9', 'H9', 'S9', 'CT', 'DT', 'HT', 'ST', 'CJ', 'DJ', 'HJ',
 'SJ', 'CQ', 'DQ', 'HQ', 'SQ', 'CK', 'DK', 'HK', 'SK', 'CA', 'DA', ...]
 
## 洗牌, 取 13 張
iex(3)> deck |> shuffle |> take(13)
['S5', 'DQ', 'ST', 'H8', 'S2', 'D9', 'CA', 'CT', 'C6', 'D6', 'D5', 'H2', 'D7']

## 發牌給四個人
iex(4)> hands = deck |> shuffle |> chunk(13)
[['HA', 'H3', 'HJ', 'C3', 'DA', 'SA', 'DQ', 'H6', 'C8', 'D6', 'D7', 'ST', 'D8'],
 ['H8', 'H5', 'C2', 'SJ', 'H2', 'S2', 'CQ', 'S4', 'S3', 'CA', 'C4', 'S8', 'H9'],
 ['DJ', 'S7', 'C5', 'CT', 'HK', 'HQ', 'C6', 'D3', 'SQ', 'D2', 'CK', 'H4', 'D4'],
 ['C9', 'D5', 'SK', 'CJ', 'S9', 'D9', 'S5', 'S6', 'DK', 'H7', 'C7', 'DT', 'HT']]

Streams - Lazy Enumerables

[ 1, 2, 3, 4, 5 ]
|> Enum.map(&(&1*&1))
|> Enum.with_index   # 產生 a list of tuples
|> Enum.map(fn {value, index} -> value - index end)
|> IO.inspect   #=> [1,3,7,13,21]

如果 words 有2.4MB,Enum 會全部 load 到記憶體並送進 split,應該改用 Stream 一行一行處理就好了

IO.puts File.read!("/usr/share/dict/words")
        |> String.split
        |> Enum.max_by(&String.length/1)

使用 Stream 會產生一個 Enum.map,而不是直接得到結果

iex(1)> s = Stream.map [1, 3, 5, 7], &(&1 + 1)
#Stream<[enum: [1, 3, 5, 7], funs: [#Function<46.40091930/1 in Stream.map/2>]]>
iex(2)> Enum.to_list s
[2, 4, 6, 8]
[1,2,3,4]
|> Stream.map(&(&1*&1))
|> Stream.map(&(&1+1))
|> Stream.filter(fn x -> rem(x,2) == 1 end)
|> Enum.to_list

大檔案資料,一次處理一行

IO.puts File.open!("/usr/share/dict/words")
        |> IO.stream(:line)
        |> Enum.max_by(&String.length/1)

也可以改為 File.stream

IO.puts File.stream!("/usr/share/dict/words") |> Enum.max_by(&String.length/1)

Infinite Stream

可產生非常大的 Stream,但只取某幾個,用 Enum 會卡住,要改用Stream

Enum.map(1..10_000_000, &(&1+1)) |> Enum.take(5)
iex(1)> Stream.map(1..10_000_000, &(&1+1)) |> Enum.take(5)
[2, 3, 4, 5, 6]

cycle, repeatedly, iterate, unfold, resource

Stream.cycle: 利用 enumerable 產生 infinite stream 包含這些 elements

iex(1)> Stream.cycle(~w{ green white }) |>
...(1)> Stream.zip(1..5) |>
...(1)> Enum.map(fn {class, value} ->
...(1)>     ~s{<tr class="#{class}"><td>#{value}</td></tr>\n} end) |>
...(1)> IO.puts
<tr class="green"><td>1</td></tr>
<tr class="white"><td>2</td></tr>
<tr class="green"><td>3</td></tr>
<tr class="white"><td>4</td></tr>
<tr class="green"><td>5</td></tr>

Stream.repeatedly: 當需要新 value 時,就會 invoke 該 function

iex(4)> Stream.repeatedly(fn -> true end) |> Enum.take(3)
[true, true, true]
iex(5)> Stream.repeatedly(&:random.uniform/0) |> Enum.take(3)
[0.4435846174457203, 0.7230402056221108, 0.94581636451987]

Stream.iterate(start_value, next_fun): 由 startvalue 開始,送入 nextfun 產生下一個 value,一直無限循環

iex(8)> Stream.iterate(0, &(&1+1)) |> Enum.take(5)
[0, 1, 2, 3, 4]
iex(9)> Stream.iterate(2, &(&1*&1)) |> Enum.take(5)
[2, 4, 16, 256, 65536]
iex(10)> Stream.iterate([], &[&1]) |> Enum.take(5)
[[], [[]], [[[]]], [[[[]]]], [[[[[]]]]]]

Stream.unfold: 產生 infinite stream of values,每一個 value 都是 previous state 的某個 function

fn state -> { stream_value, new_state } end
iex(13)> Stream.unfold({0,1}, fn {f1,f2} -> {f1, {f2, f1+f2}} end) |> Enum.take(15)
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377]

Stream.resource


defmodule Countdown do

  def sleep(seconds) do
    receive do
      after seconds*1000 -> nil
    end
  end

  def say(text) do
    spawn fn -> :os.cmd('say #{text}') end
  end

  def timer do
    Stream.resource(
      fn ->          # the number of seconds to the start of the next minute
         {_h,_m,s} = :erlang.time
         60 - s - 1
      end,

      fn             # wait for the next second, then return its countdown
        0 ->
          {:halt, 0}

        count ->
          sleep(1)
          { [inspect(count)], count - 1 }
      end,

      fn _ -> nil end   # nothing to deallocate
    )
  end
end
iex(1)> counter = Countdown.timer
#Function<50.40091930/2 in Stream.resource/3>
iex(2)> printer = counter |> Stream.each(&IO.puts/1)
#Stream<[enum: #Function<50.40091930/2 in Stream.resource/3>,
 funs: [#Function<38.40091930/1 in Stream.each/2>]]>
iex(3)> speaker = printer |> Stream.each(&Countdown.say/1)
#Stream<[enum: #Function<50.40091930/2 in Stream.resource/3>,
 funs: [#Function<38.40091930/1 in Stream.each/2>,
  #Function<38.40091930/1 in Stream.each/2>]]>
iex(4)> speaker |> Enum.take(5)
19
18
17
16
15
["19", "18", "17", "16", "15"]

Collectable Protocol

Enumerable Procotol 可 iterate over the elements Collectable Protocol 則是相反,可 insert elements 產生 collection

iex(1)> Enum.into 1..5, []
[1, 2, 3, 4, 5]
iex(2)> Enum.into 1..5, [100, 101 ]
[100, 101, 1, 2, 3, 4, 5]
iex(3)> Enum.into IO.stream(:stdio, :line), IO.stream(:stdio, :line)

Comprehensions

comprehension: 提供 1 到多個 collections,取出每個 values 的所有組合,過濾掉一些 values,用剩下的 values 產生一個新的 collection。
 result = for generator or filter… [, into: value ], do: expression

generator 的語法

pattern <- enumerable_thing
iex(1)> for x <- [ 1, 2, 3, 4, 5 ], do: x * x
[1, 4, 9, 16, 25]
iex(2)> for x <- [ 1, 2, 3, 4, 5 ], x < 4, do: x * x
[1, 4, 9]

iex(3)> for x <- [1,2], y <- [5,6], do: x * y
[5, 6, 10, 12]
iex(4)> for x <- [1,2], y <- [5,6], do: {x, y}
[{1, 5}, {1, 6}, {2, 5}, {2, 6}]
iex(5)> min_maxes = [{1,4}, {2,3}, {10, 15}]
[{1, 4}, {2, 3}, {10, 15}]
iex(6)> for {min,max} <- min_maxes, n <- min..max, do: n
[1, 2, 3, 4, 2, 3, 10, 11, 12, 13, 14, 15]
iex(8)> first8 = [ 1,2,3,4,5,6,7,8 ]
[1, 2, 3, 4, 5, 6, 7, 8]
iex(9)> for x <- first8, y <- first8, x >= y, rem(x*y, 10)==0, do: { x, y }
[{5, 2}, {5, 4}, {6, 5}, {8, 5}]

也可以用在 Bits

iex(11)> for << ch <- "hello" >>, do: ch
'hello'
iex(12)> for << ch <- "hello" >>, do: <<ch>>
["h", "e", "l", "l", "o"]
iex(14)> for << << b1::size(2), b2::size(3), b3::size(3) >> <- "hello" >>, do: "0#{b1}#{b2}#{b3}"
["0150", "0145", "0154", "0154", "0157"]

在 list comprehension 中用到的變數,都是 local 變數

iex(17)> name="You"
"You"
iex(18)> for name <- [ "cat", "dog" ], do: String.upcase(name)
["CAT", "DOG"]
iex(19)> name
"You"

comprehension 的回傳值是由 do 的結果決定

iex(1)> for x <- ~w{ cat dog }, into: %{}, do: { x, String.upcase(x) }
%{"cat" => "CAT", "dog" => "DOG"}
iex(2)> for x <- ~w{ cat dog }, into: Map.new, do: { x, String.upcase(x) }
%{"cat" => "CAT", "dog" => "DOG"}
iex(3)> for x <- ~w{ cat dog }, into: %{"ant" => "ANT"}, do: { x, String.upcase(x) }
%{"ant" => "ANT", "cat" => "CAT", "dog" => "DOG"}

iex(5)> for x <- ~w{ cat dog }, into: IO.stream(:stdio,:line), do: "<<#{x}>>\n"
<<cat>>
<<dog>>
%IO.Stream{device: :standard_io, line_or_bytes: :line, raw: false}

References

Programming Elixir

2018/04/02

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