iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 13
4

江湖上總有個傳言,說函數式編程的神人不需要 if 跟 else,只要 map, filter, reduce 就可以把事情都做完了。我覺得這個傳言該是要有中文版的澄清的時刻了。事實是,當語言有了 pattern matching 跟 guard,配合那些高階函式,就足以解決絕大部份的情況了。跟神不神人一點關係也沒有。

但是程式基本上是寫給人看的,為了要清楚表達我們的意圖,適當的使用 if else 跟 case,可以讓讀人的更明白:「嘿這裡有個分支條件,不是這樣,就是那樣。」或是「接下來的這段程式,我們要靠這個值來決定要做這些函式的其中一個。」。

Elixir 裡,就提供了 if...elsecasecond 三種語法:

if...else,但是沒有 elseif

if...else 的語法長這個樣子:

if term do
  # 當 term 為真值
else
  # 當 term 為假值
end

跟 Ruby 一樣,當 termfalse 或是 nil 時,才會跳過 if 的區塊,執行 else 區塊,其它情況皆視為真值。當然 term 也可以是個表達式,例如 == 判斷之類的。

由於 if...else 本身也是個會回傳值的表達式,所以你可以用它來進行變數綁定:

a = 
  if 0 == 1 do
    100
  else
     200
  end

#=> a = 200

Elixir 裡沒有 Ruby 跟 JavaScript 裡的三元判斷式。如果你需要短一點的寫法,你可以用上之前說過的 do: 縮寫:

a = if 0 == 1, do: 100, else: 200

if 用來表達只有某個條件符合才執行,if...else 用於表達二擇一的情況,沒有其它語言中常有的 elseif 或類似的東西。當情況有三種以上,你要改用 case

Note: 其實當 Elixir 運作時,是把 if...else 轉成只有兩種分支的 case 來執行的。

也有個反向的 unless

這顯然也是跟 Ruby 抄來致敬的。官方有建議不要在 unless 裡面放 else 區塊,因為這樣腦袋不太容易轉。 unless 是用來表示當某個條件不符合的時候,才執行該區塊:

unless 1 == 2 do
   "這是個正常的世界"
end

功能更多的 case

case 的語法如下:

case term do
  args -> do_something
end

是不是很像之前提過的函名函式?沒錯,就像匿名函式一樣,在 case 裡, 也可以使用 pattern matching 及 guards:

case {1, 2, 3} do
  {4, 5, 6} ->
    "這個不會執行"
  {z, 2, 3} when z > 100->
    "也不會執行, 第一個元素沒有大於 100"
  {1, x, 3} ->
    "這裡會把 x 綁定成 2"
  _ ->
    "如果沒有比對到,就會回傳這個"
end

首先注意一下最下面那個區塊。當 case 試著比對 term 失敗時,會拋出錯誤。因此慣例上會在最底下放一個 _ -> something 區塊,來比對所有其它的情況。還記得之前說過 _ 這個我什麼都不在乎的語法吧?

複雜條件判斷用的 cond

ifcase 都針對一個表達式進行判斷,如果你今天要處理的是多個條件的判斷,那就可以使用 cond

a = 1; b = 2; c = 3

cond do
  a == 10 && b == 5 -> 100
  b == 10 || c == 3 -> 200
  true -> 500
end

由於 cond 是判斷每個區塊箭號左手側的值是否是真值,所以它的 default 區塊要改用 true -> 來接住所有的其它情況,當然慣例上都會加這個區塊以防止錯誤發生。

cond 當然也是個表達式,可以用變數去接它求出來的值。

不是迴圈的 for

許多人在聽說 elixir 裡面沒有 for 迴圈語法,會有兩個階段的反應。一開始會覺得「那我要怎麼遍歷一個集合啊?」這一點你現在就可以回答了:用高階函式去轉換集合的形狀更加好用。非得要表達一步步執行,而且 Enum.map/2Enum.reduce/3 都不符所需的情況下,還是有 Enum.each/2 這個函式。

再過一陣子就會有人發現,明明文件裡還是有一個 for 啊!但是這個 for 跟迴圈沒有關係,他完整的名字叫做 list comprehension。這是用來給定一個起始範圍及生成條件,用來生成另一個串列的語法。可以說是 Enum.map/2 加上 Enum.filter/2 的語法糖。

先看一下示範:

for n <- [1, 2, 3], do: n * n
#=> [1, 4, 9]

for n <- 1..4, rem(n, 2) == 0, do: n * n
#=> [4, 16]

在第一個例子裡,可以看到 <- 右側是起始的串列,然後在 do 區塊裡,寫的是如何產生新的值的表達式。

而在第二個例子裡,在初始串列及 do 區塊之間,我們加了一個叫 filter 的表達式,用來判斷什麼樣的值才會被納入。另外 <- 右側可以接受 1..4 這種叫 range 的語法。

只要增加上述的規則,就可以做出更複雜的串列,甚至可以模擬巢狀迴圈產生的結果,例如:

for x <- 1..3, y <- 4..6, do: {x, y}
#=> [{1, 4}, {1, 5}, {1, 6}, {2, 4}, {2, 5}, {2, 6}, {3, 4}, {3, 5}, {3, 6}]

如果你看到有人拿 for 來處理迴圈,其實八成也能做出想要的效果,但是你要知道這原本不是設計來這樣用的。

不只能生成串列

在條件裡加上 into: 選項,還可以做出其它的集合:

for k <- [:a, :b, :c], v <- [3], into: %{}, do: {k, v * v}
#=> %{a: 9, b: 9, c: 9}

跟前輩的語法比一下: Erlang

一樣是 list comprehension,在 Erlang 裡是這樣寫的:

[X || X <- [1, 2, 3, 4], X > 2].

這是我個人少數偏好 Erlang 語法的情況。因為對比數學上的 set notation:
https://ithelp.ithome.com.tw/upload/images/20180102/20103390gZqYb59oPg.png

就很能了解這個語法當初是怎麼發想的。

N 皇后

這是個遞迴裡很有名的題目,西洋棋的皇后可以吃掉它所在的整行,整列及兩條斜線上的棋。我們想知道在 n * n 的棋盤裡,如何放進 n 個西洋棋的皇后,且它們彼此是不互相攻擊的。寫一段程式,輸入 n ,就計算出 n 個皇后在棋盤中位置的排列組合。

猜一下需要幾行?

Erlang 裡有個著名的四行解:

rows(0) -> [[]];
rows(N) ->
  [[Pos | Columns] || Columns <- rows(N-1),
                      Pos <- [1,2,3,4,5,6,7,8] -- Columns,
                      save(Pos, Columns, 1)].

save(_Pos, [], _N) -> true;
save(Pos, [Column | Columns], N) ->
  Pos /= Column + N andalso Pos /= Column - N andalso
  save(Pos, Columns, N + 1).

在 Elixir 裡,因為語法的限制,可以用完全相同的邏輯及功能,用少於十行實作出來。你已經有所有需要的工具了,試試看吧?(提示: Erlang 裡的 /= 是 Elixir 裡的 !=)

重點回顧

  • ifunless 用來表達條件符合/不符合時才執行的情況
  • if…else 用來表達二擇一的情況
  • 超過兩個條件,就要用 case
  • case 可以用 pattern matching 及 guards
  • case 最後建議放個 _ -> 預設比對區塊
  • cond 用於複雜條件判斷
  • cond 的預設比對區塊要用 true ->
  • for 不是迴圈,是生成串列(或其它集合)用的

說要輕鬆寫,結果連八皇后都出現了。明天要來解釋 struct 跟 sigil。
Happy hacking! 明天見。


上一篇
Immutability 及 Lazy evaluation
下一篇
Sigil 及 Struct
系列文
函數式編程: 從 Elixir & Phoenix 入門。31

2 則留言

0
taiansu
iT邦新手 5 級 ‧ 2018-01-01 22:35:34

部份待補 XD

難怪我覺得怎麼看到一半就沒了 有一種正精彩就斷線的感覺 XD

taiansu iT邦新手 5 級‧ 2018-01-01 23:48:29 檢舉

歹勢啦~~~ q_q

0
taiansu
iT邦新手 5 級 ‧ 2018-01-02 04:05:25

已補完

我要留言

立即登入留言