iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 4
2

具名函式:有名的都要比較正式

Elixir 中有兩種函式,具名函式及匿名函式。我們先從具名函式的語法開始:

def add_one(x) do
  x + 1
end

寫過 Ruby 的人會覺得超親切,只多了一個 do 而己。而函式名稱的慣例也跟 Ruby 一樣,會用 snake_case,也就是全小寫加底線命名。

但如本節標題所說,有名的總是比較麻煩一點。Elixir 的具名函式一定要定義在 module (模組)裡。module 簡單來說就是用 namespace 來將函式分組,避免過多的同名函式造成預期之外的行為。而 module 的命名則會用大寫開頭的 CamelCase

寫一個 elixir 檔案並執行

順便趁這機會,一步步帶大家來編寫及執行 elixir 檔案裡的函式。

  1. 先新增一個 math.exs 檔案
  2. 打開檔案並輸入以下內容
defmodule Math do
  def add_one(x) do
    x + 1
  end
end

y = Math.add_one(10)
IO.inspect(y)
  1. 回到 shell裡,輸入 elixir math.exs,可以看到輸出 11

注意 elixir 的語法中, do...end 是一組的。只要有 end,就一定會配上 do。慣例上會用兩個空白縮排,目前正在 rc 的 elixir 1.6 版將會內建排版功能,這個也會在後面的章節討論。

我們在檔案的一開始用 defmodule 定義了 Math 這個模組,這模組裡目前只定義了接收一個參數add_one 這個函式。

在模組定義之外,我們用 Math.add_one() 呼叫了函式,且傳入參數 10。將結果綁定到 y 變數上。最後用 IO.inspect 將它印出來。

IO 是 elixir 裡處理輸出輸入的模組,裡面的 IO.inspect 函式是用來監看詳細的資料結構用,我們在之後的說明裡會一直使用它,目前只要知道這樣就夠了。

比 method override 棒多了

Elixir 的函式有個更棒的特性。當你在定義 elixir 函式需要的參數 (x) 時,並不是單純的說: 這函式需要一個參數,傳進來,我會把它叫做 x。昨天我們曾提到,elixir 用單變數去 match 任何值都會成功。而 elixir 函式定義裡的參數部份,也是個 patterh matching。我們來把剛剛定義的函式上方加一個同名的函式來試試看:

defmodule Math do
  def add_one(0) do
    "I don't like zero..."
  end
  # 原先的 def add_one(x) do...
end

z = Math.add_one(0)
IO.inspect z
#=> "I don't like zero"

Elixir 中,函式的 pattern matching 是由上往下,找到能夠成功 pattern matching 參數列的函式,就呼叫該段函式。

具名函式的縮寫

如果函式本體非常短,用 do...end 的語法得要佔三行,可以改用底下這個縮寫語法:

def add_one(x), do: x + 1

也就是在函式的參數括號後方加個逗點及空白 ,,然後 do 的後方加個冒號及空白:,就可以省略掉 end,然後放在同一行。

呼叫自己,再呼叫一次,然後再一次…

所謂的遞迴,是指函式在執行的過程中呼叫自己。我們來用遞迴寫一個 Math.sum 函式來加總一個串列:

def sum([]), do: 0
def sum([h | t]), do: h + sum(t)

不要懷疑,這就是完整的程式。先看第二行,sum 會將串列的頭尾切開,用剩下串列的尾巴再一次傳進 sum 裡呼叫。再將結果跟首值相加。這個呼叫自己的行為會一直進行下去,直到串列裡沒有任何值為止。而我們用第一個函式告訴它,如果遇到空的串列,那就回傳 0

因為是第一次,我們試著扮演一下 elixir VM,用文字來一步步跑這段 code。假設我們接到了某個人類呼叫 sum([1, 2, 3]) ,開始往下走吧:

  1. [1, 2, 3] 不是空的,所以跳過第一行。
  2. 可以成功切成 [1 | [2, 3]],所以進入函式本體,綁定 h = 1t = [2,3]
  3. 我們現在手上有的東西就是 (1 + sum([2, 3]))。 這樣是沒辦法加的,我們接著對加號後面那個 sum 求值。
  4. [2, 3] 也不是空的,所以跳過第一個,進入第二個函式本體,切成 [2 | [3]] 並綁定變數。
  5. 因此我們現在手上有的東西,就是 (1 + (2 + sum([3])))。這依然不知道怎麼加,繼續對裡面那個 sum([3]) 求值。
  6. [3] 依舊不是空串列,跳過第一行。在第二行裡,會被切成… [3 | []]
  7. 所以我們現在手上有的東西裡是 (1 + (2 + ( 3 + sum([]))))。還是不會加,再來求值吧。
  8. 啊哈!第一行告訴我們說 sum([]) 要回傳 0,不囉嗦立刻把值丟回去。
  9. 我們拿著 (1 + (2 + (3 + 0))) 囉!這樣就是小學數學了,由內而外加出去,就得到結果 6 了,結案!

重點回顧

假裝成 VM 實在是太累了,今天就先這樣吧。本日我們學到的東西是:

  • 具名函式可以用 def name do...end,或是 def name(), do: 來定義
  • 具名函式要定義在 module 裡。語法是 defmodule MyModule
  • elixir 檔名 來執行檔案。
  • IO.inspect 可以印東西出來偷看。
  • 具名函式的參數也是 pattern matching,會由上往下尋找符合的函式來呼叫。
  • 遞迴就是在函式裡面呼叫自己。

這個遞迴看起來運作的很好,但是其實它有那麼一點點的問題…我們明天要來討論問題在哪,以及如何解決。

Happy hacking! 明天見。


上一篇
什麼是函數式編程,與「等號究竟是什麼意思?」
下一篇
親愛的,遞迴把記憶體塞爆了
系列文
函數式編程: 從 Elixir & Phoenix 入門。31

1 則留言

0
Bater
iT邦新手 5 級 ‧ 2017-12-23 12:16:33

原來全小寫加底線命名叫做snake_case,長知識

taiansu iT邦新手 5 級‧ 2017-12-24 02:10:23 檢舉

XD 其實就叫 snake case,只是順便展示形狀。

我要留言

立即登入留言