2018/4/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/4/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/4/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/4/9

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