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