你的程式碼聞起來很臭嗎?不用擔心!大夫幫你...越治越臭

基本資料型別與運算(Elixir從零開始系列 03)(鼠年全馬鐵人挑戰 W04)

這是 w3HexSchool 鼠年全馬鐵人挑戰 Week 4 唷

這是 Elixir 從零開始 系列 03 唷

前言

前一篇講完 function 和 module,現在我們來回頭介紹一些基本的型別系統囉

基礎型別

Numbers

整數

1
2
iex(1)> 3
3

二進位、八進位和十六進位

1
2
3
4
5
6
iex(4)> 0xff
255
iex(5)> 0b011
3
iex(6)> 0o11
9

浮點數

1
2
3
4
iex(3)> 3.14
3.14
iex(4)> 1.0e-2
0.01

一個神奇的語法糖,底線會被 Elixir 忽略,通常用來方便閱讀標記

1
2
iex(10)> 1_000_000
1000000

Atoms

Atom

Atom 是一個被命名的常數,它很類似 C++ 的 enumeration,在使用上以冒號開頭,後面搭配一個名稱,而這個名稱就代表它本身(的值),在 runtime 的時候,這個名稱就會被存在 atom table 內,當你把一個 atom 指派給某個變數的時候,這個 atom 不會直接被指派而是使用參考的方式從 atom table 中使用 atom

我對於 atom 的理解就是,把它當作一個 key-value 的 key 使用,這個 key 本身沒有意義,就只是一個名稱,只有在 key 有對應關係時才有意義,舉個例子,我想要創造一個變數叫做 name 並且把這個變數當作是 key-value 中的 key,我只要建立一個 atom 名為 :name 就可以使用它了,如下範例,這個 atom 在 map 中的關係就對應到 "Bob" 這個字串這樣子

1
2
3
4
5
:name
:age
:works_at

iex(1)> bob = %{:name => "Bob", :age => 25, :works_at => "Initech"}

以下這裡我們建立兩個 atom,可以看到這兩個(的值)是不相等的

1
2
3
4
5
6
iex(9)> :foo
:foo
iex(10)> :bat
:bat
iex(11)> :foo == :bat
false

Atoms as Booleans

在 Elixir 中,並沒有專用的 Boolean 型別,反而使用 :true:false 這兩個 atom,這裡也有一個語法糖,我們可以拿掉冒號,如下例子

1
2
3
4
iex(1)> :true == true
true
iex(2)> :false == false
true

Falsy and truthy values

在 C++ 中 的 null 在 Elixir 中是一個叫 :nil 的 atom,它跟 Booleans 一樣有語法糖可以省略冒號

1
2
iex(1)> nil == :nil
true

在 Elixir 中有一個特別的規則 falsy and truthy values,它的分類如下表格

truthy falsy
everything else nil and false

由於有這樣的一個規則存在,我們可以看到以下

1
2
iex(1)> nil || false || 5 || true
5

nilfalse 是 falsy,而 5 是 truthy,因為 || 的特性為有 true 之後的後面值就可以忽略,所以上面這行最後結果回傳是 5

strings

根據 Elixir in Action 的介紹,字串解釋如下

Elixir uses UTF-8 to encode its strings, which means that code points are encoded as a series of bytes.

因為字串是 bytes 的組合,所我們要注意一個特性,如下

1
2
iex(1)> is_binary("hello")
true

直接來看一些範例

1
2
iex(1)> "This is a string"
"This is a string"

字串插值的用法為 #{}

1
2
3
4
5
iex(2)> "Embedded expression: #{3 + 0.14}"
"Embedded expression: 3.14"
iex(3)> name = "Sean"
iex(4)> "Hello #{name}"
"Hello Sean"

字串串接,我們使用 <> 運算子來串接

1
2
3
iex(1)> name = "Sean"
iex(2)> "Hello " <> name
"Hello Sean"

可以支援多行字串,我們稱呼它 heredocs syntax

1
2
3
4
iex(9)> """
Heredoc must end on its own line
"""
"Heredoc must end on its own line\n"

新介紹!

字元列表(character list)

1
2
iex(10)> 'hełło'
[104, 101, 322, 322, 111]

使用雙引號表示為字串,使用單引號則表示為字元列表,在 Elixir 中我們通常是使用字串,會用到字元列表是因為有些純 Erlang library 用到,這裡就不多解釋

sigils

這裡還有一個語法要介紹 sigils,這個中文翻譯叫符咒?!所有的符咒將以波浪符號 ~ 開頭,已經有內建一些了,或是我們也可以自創符咒,這部分有興趣可以自行 Google,這邊我只介紹跟字串有關的符咒

  • ~S
  • ~s

小寫 s 可以處裡字串插值,而大寫就是原封不動的印出來所有的東西

1
2
3
4
iex(7)> ~S|fff #{3+0.14}|
"fff \#{3+0.14}"
iex(8)> ~s|fff #{3+0.14}|
"fff 3.14"

用來分隔符號清單如下,這些都可以用

  • <...> 尖括號 (pointy brackets)
  • {...} 大括號 (curly brackets)
  • [...] 中括號 (square brackets)
  • (...) 小括號 (parentheses)
  • |...| 管線符號 (pipes)
  • /.../ 斜線 (forward slashes)
  • "..." 雙引號 (double quotes)
  • '...' 單引號 (single quotes)
1
2
3
4
5
6
iex(3)> ~s/the cat in the hat on the mat/
"the cat in the hat on the mat"
iex(4)> ~s(This is also a string)
"This is also a string"
iex(5)> ~s|This is also a string|
"This is also a string"

(Linked) Lists

列表(List)是資料的群集,使用中括號 [...] 定義,裡面可以包含任何型態唷

1
2
3
4
iex(1)> [3.14, :pie, "Apple"]
[3.14, :pie, "Apple"]
iex(2)> prime_numbers = [2, 3, 5, 7]
[2, 3, 5, 7]

列表看起來像是 array 但是它其實比較像是 singly linked list,所以多數針對列表的操作都是 O(n),比如說函式 Kernel.length/1 或是 Enum.at/2,這兩個函式的操作都要從頭開始執行,時間複雜度就是 O(n),因此,有一個特性要特別提醒的就是,通常前置插入 (prepend) 比後綴置入 (append) 更快

以下是 O(n) 操作

1
2
3
4
5
iex(2)> length(prime_numbers)
4

iex(3)> length(prime_numbers)
4

以下是 prepend vs append

1
2
3
4
5
6
7
8
iex(4)> list = [3.14, :pie, "Apple"]
[3.14, :pie, "Apple"]
# Prepending (fast)
iex(5)> ["π" | list]
["π", 3.14, :pie, "Apple"]
# Appending (slow)
iex(6)> list ++ ["Cherry"]
[3.14, :pie, "Apple", "Cherry"]

列表串接使用 ++/2 運算子

1
2
iex(11)> [1, 2, 3] ++ [4, 5]
[1, 2, 3, 4, 5]

通過提供 --/2 運算子支援減法;即使減去不存在的值也是安全的

1
2
iex(12)> ["foo", :bar, 42] -- [42, "bar"]
["foo", :bar]

注意重複的值。對於右邊的每個元素,左邊中第一個出現的將被移除

1
2
iex(13)> [1,2,2,3,2,3] -- [1,2,3,2]
[2, 3]

注意!

減法為嚴格匹配

1
2
3
4
iex(1)> [2] -- [2.0]
[2]
iex(2)> [2.0] -- [2.0]
[]

使用列表時,常常操作列表的頭和尾。 頭是列表的第一個元素,而尾是包含剩餘元素的列表。 在這個部份的操作中 Elixir 提供了兩個有用的函式 hdtl

1
2
3
4
iex(14)> hd [3.14, :pie, "Apple"]
3.14
iex(15)> tl [3.14, :pie, "Apple"]
[:pie, "Apple"]

Tuples

中文翻譯成元組?!我覺得這翻譯很奇怪就是了。這是一組沒有型態的結構,使用大括號 {...} 來定義

1
2
iex(1)> person = {"Bob", 25}
{"Bob", 25}

我們可以很常看到用來做為函式回傳接收

1
2
3
4
iex(1)> File.read("path/to/existing/file")
{:ok, "... contents ..."}
iex(2)> File.read("path/to/unknown/file")
{:error, :enoent}

Maps

映射是一組 key-value 的對應關係,我們使用 %{} 來定義它,並使用 => 來做對應

1
2
3
4
5
6
7
8
9
10
11
12
iex(2)> squares = %{1 => 1, 2 => 4, 3 => 9}
iex(3)> squares[2]
4
iex(4)>
nil

iex(5)> map = %{:foo => "bar", "hello" => :world}
%{:foo => "bar", "hello" => :world}
iex(6)> map[:foo]
"bar"
iex(7)> map["hello"]
:world

我們也可這樣用

1
2
iex(8)> map.foo
"bar"

2020/03/17 update:

有另外一種特殊的語法,使用冒號來區分,如下範例

1
2
3
4
iex> %{foo: "bar", hello: "world"}
%{foo: "bar", hello: "world"}
iex> %{foo: "bar", hello: "world"} == %{:foo => "bar", :hello => "world"}
true

Keyword lists

關鍵字列表其實是上面介紹過的 list 和 tuple 的組合,我們直接看範例

1
[ method: "", path: "", resp_body: "", status: nil ]

事實上在內部表示為

1
[ {:method, ""}, {:path, ""}, {:resp_body, ""}, {:status, nil} ]

所以呢,我們可以總結幾點

  • 關鍵字列表就是一個列表(list)
  • 列表內的元素全部都是 tuple
  • Tuple 的第一個元素必須為 atom

基礎運算

基本的四則運算不可少!

1
2
3
4
5
6
7
8
iex(1)> 2 + 2
4
iex(2)> 2 - 1
1
iex(3)> 2 * 5
10
iex(4)> 10 / 5
2.0

這裡的除法 永遠會回傳浮點數

如果要取餘數或是整數的除法的話可以用以下方法

1
2
3
4
iex(1)> div(10, 5)
2
iex(2)> rem(10, 3)
1

andornot,搭配這三個操作的第一個參數必須是 truefalse

1
2
3
4
5
6
7
8
9
10
11
12
13
iex(23)> true and 'a'
'a'
iex(24)> false and 'a'
false
iex(25)> not false
true
iex(26)> false or 'a'
'a'
iex(26)> 12 and 3
** (ArgumentError) argument error: 12
iex(26)> not 12
** (ArgumentError) argument error
:erlang.not(12)

Elixir 提供 ||&&!

|| 運算子會回傳第一個不是 falsy 的值

&& 運算子,如果第一個表達式為 truthy 則會回傳第二個表達式,反之,回傳第一個表達式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
iex(1)> -20 || true
-20
iex(2)> false || 42
42

iex(3)> 42 && true
true
iex(4)> 42 && nil
nil

iex(5)> !42
false
iex(6)> !false
true

比較運算子: ==!====!==<=>=<>

1
2
3
4
5
6
7
8
iex(1)> 1 > 2
false
iex(2)> 1 != 2
true
iex(3)> 2 == 2
true
iex(4)> 2 <= 3
true

為了嚴謹比較整數和浮點數,請使用 ===

1
2
3
4
iex(5)> 2 == 2.0
true
iex(6)> 2 === 2.0
false

Elixir 的一個重要特點是可以比較任意兩種型別;這在排序中特別有用。我們不需要記住排序順序,但還是要注意它的重要性

1
number < atom < reference < function < port < pid < tuple < map < list < bitstring

這個特點可能會導致一些詭異但合乎語法,而您在其他語言中找不到的的比較運算

1
2
3
4
iex(1)> :hello > 999
true
iex(2)> {:hello, :world} > [1, 2, 3]
false

參考資料

  1. https://elixirschool.com/zh-hant/lessons/basics/basics/
  2. https://docs.microsoft.com/zh-tw/cpp/cpp/enumerations-cpp?view=vs-2019
  3. http://blog.zhulinpinyu.com/2017/07/04/elixir-keyword-list/