2018/04/16

Elixir4 StringsAndBinaries

String Literals

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

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

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

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

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

sigil

以下為 sigil types

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

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

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

也可使用 """

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

single-quoted strings: lists of character codes

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

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

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

defmodule Parse do

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

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

Binaries

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

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

可儲存 integers, floats, binaries

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

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

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

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

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

使用 string(binary) 的 elixir library

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

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

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


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

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

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

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


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


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

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


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

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

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


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

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


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


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


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


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


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

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

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


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


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

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

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

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

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

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

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

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

Binaries and Pattern Matching

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

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

以 Binary 方式處理 String

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

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

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

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

Control Flow

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

if, unless

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

cond

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

defmodule FizzBuzz do

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

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

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

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

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

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

defmodule FizzBuzz do

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

  defp _downto(0, result),  do: result

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

改用 Enum.map 重複呼叫 fizzbuzz

defmodule FizzBuzz do

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

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

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

defmodule FizzBuzz do

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

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

end

case

File.open 會遇到兩種 return

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

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

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

end

加上 guard clause

defrecord Person, name: "", age: 0

defmodule Bouncer do

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

  case dave do

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

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

  end
end

Expcetions

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

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

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

錯誤時 raise Exception

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

或是由 pattern matching 的方式處理

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

References

Programming Elixir

2018/04/09

Elixir 3 Lists_Maps

Lists


Heads and Tails

[] 為 empty list

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

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

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

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

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

運算過程為

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

Map function for list

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

使用 map

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

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

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

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

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

defmodule MyList do

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

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

list 的 reduce function 定義

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

使用 reduce.exs

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

More complex List Patterns

defmodule Swapper do

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

end

執行結果

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

Lists of Lists

defmodule WeatherHistory do

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


  # standard recurse until the list is empty stanza

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

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

改進 weather2.exs

defmodule WeatherHistory do

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

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

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

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

end

weather3.exs

defmodule WeatherHistory do

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

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

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

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

end

functions in List Module

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

Dictionary: Map, Keyword Lists, Sets, Structs

dictionary: a data type that associates keys with values


選用 Maps 或是 Keyword Lists?

依序考慮下列問題

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

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

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

  4. 最後,就直接用 map

Keyword Lists

defmodule Canvas do

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

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

end

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

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

Keyword List 可使用 KeywordEnum modules 的所有 functions

Maps

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

Pattern Matching and Updating Maps

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

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

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

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

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

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

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

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

defmodule HotelRoom do

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

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

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

end

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

結果為

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

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

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

Pattern Matching 可 match variable keys

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

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

Updating Maps 最簡單的方式為

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

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

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

Structs

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

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

使用 Subscriber 的方式

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

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

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

Nested Ductionary Structures

可在 value 的部分使用 dictionary


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

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

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

  IO.inspect report

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

  IO.inspect report

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

執行結果

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

Nested Accessors and Nonstructs

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

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

Dynamic (Runtime) Nested Accessors

get_in
put_in
update_in
get_and_update_in

ex:


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

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

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

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

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

可利用 fun 當作 keys 傳入 get_in

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

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

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

Access Module

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

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


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

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

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

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

Access.elem 可處理 tuples

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

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

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

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


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

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

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

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

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

Sets

Sets 是以 MapSet module 實作的

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

Enum and Stream

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

Enum

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

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

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


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

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

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

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


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

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

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

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

處理撲克牌

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

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

Streams - Lazy Enumerables

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

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

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

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

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

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

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

也可以改為 File.stream

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

Infinite Stream

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

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

cycle, repeatedly, iterate, unfold, resource

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

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

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

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

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

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

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

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

Stream.resource


defmodule Countdown do

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

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

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

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

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

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

Collectable Protocol

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

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

Comprehensions

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

generator 的語法

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

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

也可以用在 Bits

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

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

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

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

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

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

References

Programming Elixir

2018/04/02

Elixir 2 - Modules, Functions

Anonymous Functions

fn
    parameter-list -> body
    parameter-list -> body ...
end
iex(1)> sum = fn (a, b) -> a + b end
#Function<12.99386804/2 in :erl_eval.expr/5>
iex(2)> sum.(1, 2)
3

# 可省略 ()
iex(3)> sum = fn a, b -> a + b end
#Function<12.99386804/2 in :erl_eval.expr/5>

# 沒有參數的 fn
iex(5)> f2 = fn -> 99 end
#Function<20.99386804/0 in :erl_eval.expr/5>
iex(6)> f2.()
99

以 tuple 為參數

iex(8)> swap = fn { a, b } -> { b, a } end
#Function<6.99386804/1 in :erl_eval.expr/5>
iex(9)> swap.({1,2})
{2, 1}

一個 fn 可以有多種 function body,fn 是以 File.open 回傳的結果決定 fn body 的內容是使用哪一個 pattern 的定義

iex(1)> open_file = fn
...(1)>     {:ok, file} -> "Read data: #{IO.read(file, :line)}"
...(1)>     {_, error} -> "Error: #{:file.format_error(error)}"
...(1)>   end
#Function<6.99386804/1 in :erl_eval.expr/5>
iex(2)> open_file.(File.open("/etc/passwd"))
"Read data: ##\n"
iex(3)>   open_file.(File.open("nonexistent"))
"Error: no such file or directory"

可以 Return Function

iex(6)> fun1 = fn -> fn -> "Hello" end end
### 等同 fun1 = fn -> (fn -> "Hello" end) end
#Function<20.99386804/0 in :erl_eval.expr/5>
iex(7)> fun1.()
#Function<20.99386804/0 in :erl_eval.expr/5>
iex(8)> fun1.().()
"Hello"

Functions Remember Their Original Environment

iex(13)> greeter = fn name -> (fn -> "Hello #{name}" end) end
#Function<6.99386804/1 in :erl_eval.expr/5>
iex(14)> dave_greeter = greeter.("Dave")
#Function<20.99386804/0 in :erl_eval.expr/5>
iex(15)> dave_greeter.()
"Hello Dave"

Parameterized Functions

iex(19)> add_n = fn n -> (fn other -> n + other end) end
#Function<6.99386804/1 in :erl_eval.expr/5>
iex(20)> add_two = add_n.(2)
#Function<6.99386804/1 in :erl_eval.expr/5>
iex(21)> add_five = add_n.(5)
#Function<6.99386804/1 in :erl_eval.expr/5>
iex(22)> add_two.(3)
5
iex(23)> add_five.(7)
12

以 fun 為參數

iex(26)> times_2 = fn n -> n * 2 end
#Function<6.99386804/1 in :erl_eval.expr/5>
iex(27)> apply = fn (fun, value) -> fun.(value) end
#Function<12.99386804/2 in :erl_eval.expr/5>
iex(28)> apply.(times_2, 6)
12

利用 Pin operator 將 fun 固定參數

defmodule Greeter do
  def for(name, greeting) do
    fn
      (^name) -> "#{greeting} #{name}"
      (_) -> "I don't know you"
    end
  end
end
mr_greeter = Greeter.for("Test", "You")
IO.puts mr_greeter.("Test") # => You Test
IO.puts mr_greeter.("Dave") # => I don't know you

利用 & 實作 func

add_one = &(&1 + 1)
# & 就是 fn
# &1 是 fn 的第一個參數, &2 是 fn 的第二個參數

### 等同
add_one = fn (n) -> n + 1 end
iex(38)> divrem = &{ div(&1,&2), rem(&1,&2) }
#Function<12.99386804/2 in :erl_eval.expr/5>
iex(39)> divrem.(13, 5)
{2, 3}
iex(42)> l = &length/1
&:erlang.length/1
iex(43)> l.([1,3,5,7])
4

& 可快速產生一個 anonymous function 傳入 map function 的第二個參數

iex(44)> Enum.map [1,2,3,4], &(&1 + 1)
[2, 3, 4, 5]

Modules & Named Functions

times.exs

defmodule Times do
  def double(n) do
    n * 2
  end
end
$ iex times.exs
iex(1)> Times.double
double/1
iex(1)> Times.double(3)
6

也可以啟動 iex 後,用 c 編譯

iex(1)> c("times.exs")
[Times]

## 可直接知道是哪一行,哪一個 function 發生錯誤
iex(2)> Times.double("2")
** (ArithmeticError) bad argument in arithmetic expression
    times.exs:3: Times.double/1

do: 是 syntax sugar,可以用 () 將 expression grouping 在一起, do: 就是 do ... end 的簡化語法

defmodule Times do
  def double(n), do: n * 2
  
  def greet(greeting, name), do: (
    IO.puts greeting
    IO.puts "How're you doing, #{name}?"
  )
end

Function Call with Pattern Matching

defmodule Factorial do
  def of(0), do: 1
  def of(n), do: n * of(n-1)
end
iex(1)> Factorial.of(3)
6
iex(2)> Factorial.of(100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

def of(0), do: 1 不能寫在後面,因為 elixir 是依照順序檢查是否有符合 pattern,如果寫錯了,compiler 會出現警告

defmodule BadFactorial do
  def of(n), do: n * of(n-1)
  def of(0), do: 1
end

warning: this clause cannot match because a previous clause at line 2 always matches
  factorial1-bad.exs:3

Guard Clauses

defmodule Guard do

  def what_is(x) when is_number(x) do
    IO.puts "#{x} is a number"
  end

  def what_is(x) when is_list(x) do
    IO.puts "#{inspect(x)} is a list"
  end

  def what_is(x) when is_atom(x) do
    IO.puts "#{x} is an atom"
  end

end

Guard.what_is(99)        # => 99 is a number
Guard.what_is(:cat)      # => cat is an atom
Guard.what_is([1,2,3])   # => [1,2,3] is a list

回頭看剛剛的 factorial,如果 n 為負數,就會發生無窮迴圈的狀況,因此要改為

defmodule Factorial do
  def of(0), do: 1
  def of(n) when n > 0 do
    n * of(n-1)
  end
end

在 guard clause 中可以使用的 elixir expression

  1. comparison operator

    ==, !=, ===, !==, >, <, <=, >=

  2. Boolean and negation operators

    or, and, not, ! 不能使用 || and &&

  3. Arithmetic operators

    +, -, *, /

  4. Join operators

    <> and ++, as long as the left side is a literal.

  5. in operator

    Membership in a collection or range

  6. Type-check functions

    is_atom is_binary is_bitstring is_boolean is_exception is_float is_function is_integer is_list is_map is_number is_pid is_port is_record is_reference is_tuple
  7. Other functions

    回傳 value 的 built-in functions

    abs(number) bit_size(bitstring) byte_size(bitstring) div(number,number) elem(tuple, n) float(term) hd(list) length(list) node() node(pid|ref|port) rem(number,number) round(number) self() tl(list)trunc(number) tuple_size(tuple)

參數的預設值

defmodule Example do

  def func(p1, p2 \\ 2, p3 \\ 3, p4) do
    IO.inspect [p1, p2, p3, p4]
  end
end

Example.func("a", "b")             # => ["a",2,3,"b"]
Example.func("a", "b", "c")        # => ["a","b",3,"c"]
Example.func("a", "b", "c", "d")   # => ["a","b","c","d"]

因為前面已經定義了 2, 3, 4 個參數的 func,因此 def func(p1, p2) 會出現重複 func 定義的 error

defmodule Example do

  def func(p1, p2 \\ 2, p3 \\ 3, p4) do
    IO.inspect [p1, p2, p3, p4]
  end

  def func(p1, p2) do
     IO.inspect [p1, p2]
  end

end


** (CompileError) default_params.exs:7: def func/2 conflicts with defaults from def func/4
    default_params.exs:7: (module)
    default_params.exs:1: (file)

以沒有 func body 的 func 定義,來解決剛剛遇到的問題

defmodule Params do
  def func(p1, p2 \\ 123)

  def func(p1, p2) when is_list(p1) do
    "You said #{p2} with a list"
  end

  def func(p1, p2) do
    "You passed in #{p1} and #{p2}"
    end
end

IO.puts Params.func(99) # You passed in 99 and 123
IO.puts Params.func(99, "cat") # You passed in 99 and cat
IO.puts Params.func([99]) # You said 123 with a list
IO.puts Params.func([99], "dog") # You said dog with a list

defp 可定義 Private Functions,但不能 private 與 public function 不能共用


Pipe Operator |>

以往會寫成

people = DB.find_customers
orders = Orders.for_customers(people)
tax = sales_tax(orders, 2016)
filing = prepare_filing(tax)

或是

filing = prepare_filing(sales_tax(Orders.for_customers(DB.find_customers), 2016))

但有了 |> 可改成

filing = DB.find_customers
    |> Orders.for_customers
    |> sales_tax(2016)
    |> prepare_filing
val |> f(a,b)

等同
f(val,a,b)
list
    |> sales_tax(2016)
    |> prepare_filing

等同

prepare_filing(sales_tax(list, 2016))

& 就是 fun

iex> (1..10) |> Enum.map(&(&1*&1)) |> Enum.filter(&(&1 < 40))
[1, 4, 9, 16, 25, 36]

Modules

defmodule Mod do
    def func1 do
        IO.puts "in func1"
    end
    def func2 do
        func1
        IO.puts "in func2"
    end
end

Mod.func1
Mod.func2

Inner Module

defmodule Outer do
    defmodule Inner do
        def inner_func do
        end
    end
    
    def outer_func do
        Inner.inner_func
    end
end

Outer.outer_func
Outer.Inner.inner_func

mix/tasks/doctest.ex

defmodule Mix.Tasks.Doctest do
    def run do
    end
end

Mix.Tasks.Doctest.run

Directives for Modules

  1. import

    語法 import Module [, only:|except: ]

    ex: import List, only: [ flatten: 1, duplicate: 2 ]

    defmodule Example do
      def func1 do
         List.flatten [1,[2,3],4]
      end
      def func2 do
        import List, only: [flatten: 1]
        flatten [5,[6,7],8]
      end
    end
  2. alias

    defmodule Example do
        def compile_and_go(source) do
            alias My.Other.Module.Parser, as: Parser
            alias My.Other.Module.Runner, as: Runner
            source
                |> Parser.parse()
                |> Runner.execute()
        end
    end

    也可寫成

    alias My.Other.Module.Parser
    alias My.Other.Module.Runner
    
    alias My.Other.Module.{Parser, Runner}
  3. require

    當寫下 require a module,在編譯時可保證一定存在該 macrio 定義


Module Attribute

只能用在 module 的最底層,這不是變數,比較接近 configuration, metadata 的用途

defmodule Example do

    @author  "Test"
    def get_author do
        @author
    end

    @attr "one"
    def first, do: @attr
    @attr "two"
    def second, do: @attr

end

module name 就是 atoms,因此 IO 會被轉換為 Elixir.IO

iex(1)> is_atom IO
true
iex(2)> to_string IO
"Elixir.IO"
iex(3)> :"Elixir.IO" === IO
true
iex(4)> IO.puts 123
123
:ok
iex(5)> :"Elixir.IO".puts 123
123
:ok

如何呼叫 Erlang Library 的 Functions

erlang 的 io module,就是 elixir 的一個 atom :io

iex(16)> :io.format("The number is ~3.1f~n", [5.678])
The number is 5.7

References

Programming Elixir