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