Elixir Basic Introduction
會想學 Elixir 是想知道 functional programming 寫起來的手感,剛好公司有其他專案在使用,想要順便累積經驗,先學些基礎就可以從一些小票開始做起!
這篇主要在寫 Elixir 的基本,最可怕最精華的 OTP 還無法寫,因為還不會 XD
上了這個 課程 之後了解一些基本概念,也感謝泰安老師開過 Elixir 的 workshop,讓我知道這個課程沒介紹到 Elixir 最精華的部分 QQ
希望之後如果多學到了什麼會再補充(每次都這樣講,但後來都沒時間補)
Concept
在 functional programming 裡面,module 就是包著很多的 function 的集合,沒有 instance variable 的概念
在 elixir 裡面可以有多個同名的 function,但有不同的參數數量,在 elixir 裡面會認為是不同的 function, Cards.shuffle/0 跟 Cards.shuffle/1 是不同的 functions
在 elixir 裡面如果定義一個有 default 值的 function,其實總共一次做了兩個 function,他們接的參數數量不同
所以如果 looping 產生 atom 就會 memory leak
pattern matching
在 elixir 裡面很多取值的行為都要透過 pattern matching 的方式實現,這點對於習慣物件導向的人(像是我)可能比較不好適應
在等號左右邊,只要資料結構相同(ex. tuple 對上 tuple),而且資料的數目相同,那就可以做 pattern matching,又或者說,其實每次在使用等號都在做 pattern matching
比方說 Enum.split(deck)
的結果會變成 {my_hand, the_rest}
這樣的結構
如果用 Enum.split(deck)[0]
這種方式會出錯
而是要用 {hand, rest_of_deck} = Enum.split(deck)
這種方式把東西 assign 給 hand 跟 reset_of_deck 這兩個額外的變數
1 | iex(2)> color |
如果是 map 比 map, 只要前面是後面的子集都可以成功
1 | > html=%{head: "<html5>", status: "100", body: "some_str"} |
Case
在 elixir 的 case 裡面同樣是以 pattern matching 的方式進行
1 | def load(filename) do |
然後這有更優雅的寫法,我們已經知道 File.read 會產生一個狀態跟副產品,就可以不用在外面先做一次 assignment
1 | def load(filename) do |
pipe operator
因為不是物件導向的關係,我們很有可能會寫出下面這種 code
1 | def create_hand(hand_size) do |
pipe operator 可以讓這種 code 變得很簡潔,他會把上一個產生的結果自動塞到下一個式子的第一個 argument
1 | def create_hand(hand_size) do |
跟 Erlang 的關係
其實 Elixir 就像是提供一個比較容易操作的介面讓我們操作 Erlang
Elixir 跟 Erlang 之間的關係,比較初階的概念圖可以看下面這張圖,但其實沒有很精確
實際上 Elixir 會 transpile 變成 Erlang Abstract format 最後變成 beam file,然後由 BEAM(Erlang virtual machine) 去執行,有興趣的可以看看我找到的一篇 討論
也因為這樣,有一些 Elixir 沒有的 library 可能需要靠 Erlang 的協助,像是畫圖就可以用 Erlang 的 egd module
下面這是另一個例子:
1 | binary = :erlang.term_to_binary(deck) |
通常 erlang 的 module 都是小寫,而 Elixir 的 module 都是大寫
常用的 module
Enum
像是 map 或者 filter 這種 function,後面接另一個匿名函式:
1 | def filter_odd_sqaures(%Identicon.Image{grid: grid} = image) do |
如果要用 map 傳給另一個有名字的 function,有個像是 ruby 的寫法
1 | def build_grid(%Identicon.Image{hex: hex} = image) do |
type
atom
像是 ruby 的 symbol
1 | :some_atom |
要注意 Elixir 裡面的 atom 不做垃圾回收,所以不要動態的產生 atom,會造成 memory leak
string
1 | "string" |
字串只能用 double quote
1 | > [97, 98, 99] |
如果一個 array 裡面的數字都是 ASCII 守備範圍,會把它變成字
1 | > [83,84,85,86,87,88,89,90,91] |
list
在 elixir 裡面沒有 array
我們看到的 array 其實只是把它變成我們容易理解的樣子
1 | [1, 2, 3] |
因為巢狀結構的關係,所以塞東西到 list 裡面最好從前面塞,不然 performance 很慢
The performance of getting nth element in a list is O(n)
可以用 ++ 把東西塞到 list 裡面
在 functional language 裡面,通常一個 array 存到記憶體裡面就不會再去改變
所以下面的範例會佔據三個記憶體空間
1 | [1, 2, 3] ++ [4,5,6] |
for loop of list
如果用 <- 這個符號,表示把 list 裡面每一個東西都做迭代
他會把原本的 list 的東西丟到 do block 裡面,最後產生新的 list
1 | def create_deck do |
comprehension of list 還可以同時進行兩個回圈
1 | def create_deck do |
如果像是 ruby 那樣包成兩層,結果會是 nested 的 list,所以可能跟我們想要的不同
tuple
在 elixir 裡面 tuple 長度需要是固定的,如果去做 insert 這些操作,都會產生全新的 tuple
1 | tuple = { :a, :b, :c, :d} |
那到底什麼時候要用 list,什麼時候用 tuple 呢?
這篇文件 有詳細的解說
簡單來說 list 就有前面說的,操作越後面的 element performance 會越差,這點在 tuple 身上就不會,但是如果要更新 tuple 的代價昂貴,因為他會產生一個新的 tuple 存起來
map
就像是 ruby 的 hash
1 | %{a:1, b:2, c:3} |
key 可以是任何東西
1 | %{"a" => 1, 2 => "b", [1,2,3] => [4,5,6]} |
map 的 key 如果是 symbol 的話可以用 .
去拿到 value
但如果是字串當作 key 的話,要拿到 value 需要用 pattern matching
map 的 pattern matching:
左右邊不一定要相同,但左邊的 key 是一定要在這個 map 中存在的
1 | > m = %{:a => 1, "b" => 2} |
1 | iex(1)> colors = %{primary: "red", secondary: "blue"} |
如果是 map 或者 struct,他的 key 使用 symbol,則可以用 . 的方式(屬性)去拿到 nested 的值
1 | > mm = %{primary: %{a: 1, b: 2}, secondary: "blue"} |
更新 map
其實在 elixir 裡面不會去改變一個 data structure 的直,而是把原本的複製一份做一個新的出來
1 | iex(1)> colors = %{primary: "red", secondary: "blue"} |
或者可以用 pipe 來更新 map 的值
1 | iex(4)> %{colors | primary: "blue"} |
但這只適用在 map 裡面有這個值的時候,如果 map 原本沒有這個 key, 就必須用 put 放到 map 裡面
Keyword List
還有一個特殊的資料結構叫做 keyword list
1 | [{:a, 1}, {:b, 2}, {:c, 3}] |
他是一個 list + tuple 組合起來的資料結構
1 | iex(6)> colors = [{:primary, "red"},{:secondary, "blue"}] |
在 ecto 裡面常常用到這個結構
1 | query = User.find_where([where: user.age > 10, where: user.subscribed == true]) |
另外 elixir 還有一個特殊規則,如果傳到 function 裡面的最後一個參數是 keyword list,那他的中括號可以省略,所以又可以寫成這樣:
1 | query = User.find_where(where: user.age > 10, where: user.subscribed == true) |
Struct
struct 又是另一種資料結構,很像map,我們可以在 module 裡面定義,然後使用的時候前面加上 %
1 | defmodule Identicon.Image do |
struct 也可以做 pattern matching
很特別的是要把 Struct 前面的類似 namespace 也都寫上去
1 | defmodule Identicon do |
其中,我們也可以省略掉 assign 給 hex_list 這個步驟
1 | def pick_color(image) do |
struct 比較特別的地方是要在他身上先定義好之後會有的 key,不能想加就加,否則會報錯
現在我們嘗試把 rgb 三個變數包在原本的 struct 裡面丟回去,首先要重新定義 struct,加上 color 這個 key
1 | defmodule Identicon.Image do |
延續上面的範例,但這次我們要把回傳值改成 struct,然後 struct 跟 map 一樣可以用 pipe 去改變原本就有的 key 的值
1 | def pick_color(image) do |
而我們甚至可以再接收到參數當下就做 pattern matching
1 | def pick_color(%Identicon.Image{hex: [r, g, b | _tail] } = image) do |
改寫成這樣的話,還是接收一個參數,但它的意義在於除了接收參數,還想同時做 pattern matching
Elixir 工具箱
iex
iex 是 iteractive elixir shell
iex -S mix
就很像 rails 的 rails c 依樣
就是把這個 console 掛進去專案裡面
Mix
mix 是 elixir 內建的 command line tool
mix 像是 ruby 的 bundler / Rspec / Rake 的集合體
1 | > mix new <project name> |
Mix file
有個檔案檔名是 mix.exs
小知識: 帶有 .ex
副檔名的檔案會先 compile 成 .beam
檔案再去執行,而帶有 .exs
的檔案代表 compile 完直接在記憶體裡面執行
這裡面 deps 的地方專門拿來放第三方套件
如果要裝的話就在 command line 下 mix deps.get
(像是 bundle install)
1 | defp deps do |
xdoc
xdoc 是專門做文件用的套件, 我們只要在想要加上文件的 module 裡面這樣寫:
1 | defmodule Cards do |
如果是要做 function 的文件,則改用 @doc 關鍵字
,然後如果要加上 code 的話,格式需要非常注意
1 | @doc """ |
接著在 terminal 下 mix docs
而 xdoc 還有一個很酷的地方是,他裡面寫的這些範例會自動被當作測試去測,所以文件永遠不會過期
如果要單獨執行測試,可以下 mix test
除了寫在檔案本身的 xdoc 內容之外,我們也可以寫在 test 目錄底下的檔案
1 | defmodule CardsTest do |
其中,除了 assert 可以用之外,也可以用 refute 來做反向驗證
延伸資源
準備環境可以參考泰安老師的 文章
這次上的 udemy 課程
Elixir school
官方文件
Thinking In Ecto
OTP 介紹影片
可以線上試 elixir 語法的網站