Comprehension 表示
February 2, 2022數學會使用集合建構式符號來描述集合(Set),像是有 10 個元素的 5 倍數集合可以表示為 {5 * x | x ∈ N, x ≤ 10},這也稱為 Set Comprehension。
如果有個需求是產生一個 list,其中的元素就是 {5 * x | x ∈ N, x ≤ 10},根據〈惰性求值〉的說明,可以使用 take 10 $ [5, 10 ..]
,這就會建立 [5,10,15,20,25,30,35,40,45,50]
,不過,若寫成 [5 * x | x <- [1 .. 10]]
,也能得到相同的結果,寫法與 Set Comprehension 類似,一看就懂,因為產生的結果是個 list,因而稱為 List Comprehension。
就如從 Set Comprehension 可一眼就看出集合定義,某些 Haskell 程式碼可以改寫為 List Comprehension,讓程式的意圖更明顯,就像方才將 take 10 $ [5, 10 ..]
改為 [5 * x | x <- [1 .. 10]]
那樣。
map 與 filter
List Comprehension 能做到 map
函式的功能,例如 map (+ 3) [1, 2, 3, 4, 5]
,也可以使用 [x + 3 | x <- [1, 2, 3, 4, 5]]
,也能使用 List Comprehension 來實作 map'
函式:
map' :: (a -> a) -> [a] -> [a]
map' mapper lt = [mapper elem | elem <- lt]
如果 List Comprehension 加上條件式,可以達到單純過濾元素的功能,例如 filter (> 3) [1, 2, 3, 4, 5]
,也可以使用 [x | x <- [1, 2, 3, 4, 5], x > 3]
來得到,也能使用 List Comprehension 來實作 filter'
函式:
filter' :: (a -> Bool) -> [a] -> [a]
filter' predicate lt = [elem | elem <- lt, predicate elem]
就簡單的對應與過濾來說,使用 [x + 3 | x <- [1, 2, 3, 4, 5]]
與 [x | x <- [1, 2, 3, 4, 5], x > 3]
,不見得比 map (+ 3) [1, 2, 3, 4, 5]
與 filter (> 3) [1, 2, 3, 4, 5]
來得清楚,不過,如果是 map (+ 3) $ filter (> 3) [1, 2, 3, 4, 5]
與 [x + 3 | x <- [1, 2, 3, 4, 5], x > 3]
相比,那後者倒是就清楚許多。
更多 List Comprehension
來看看更多 List Comprehension 的語法,如果在取得 List 中的元素時,實際上不需要元素值,那麼 <-
左邊可以使用 _
,例如,[1 | x <- [1, 2, 3, 4, 5]]
,可以寫成 [1 | _ <- [1, 2, 3, 4, 5]]
,嗯?這是什麼?例如,來用 List Comprehension 寫個 leng
函式:
leng :: [a] -> Int
leng lt = sum [1 | _ <- lt]
每取得一個元素就計數 1
,最後加總就是 list 的長度,如果要取得三邊長不大於 10
的直角三角形呢?三角不等式定義是兩邊之和大於第三邊,而直角三角型是兩邊的平方和等於斜邊平方。如果你要用 filter
、map
兜出這個需求可不容易,如果用 List Comprehension,三邊長不大於 10
,可以使用三個 [1..10]
列舉,若以其中一個 [1..10]
作為最長邊,依三角不等式定義,使用 List Comprehension 可以寫成:
[[a, b, c] |
a <- [1..10], b <- [1..10], c <- [1..10],
a <= b, b <= c]
List Comprehension 可以列舉多個 list,只要以 ,
做區隔,接下來加上直角三角型定義:
[[a, b, c] |
a <- [1..10], b <- [1..10], c <- [1..10],
a <= b, b <= c,
a ^2 + b ^ 2 == c ^ 2]
最後得到 [[3,4,5],[6,8,10]]
的結果,只要寫下問題定義,Haskell 就會為你求解。
命令式?宣告式?
只要寫下問題定義,Haskell 就會為你求解,這樣的風格就是宣告式(Declarative),這是函數式程式設計與命令式(Imperative)程式設計不同之處,在宣告式的設計中,你著重的是 WHAT,而不是 HOW,就像上面求直角三角形的例子,你著重的是「直角三角形是什麼?」,而不是「如何找出直角三角形」,如果是命令式求解,例如 Java 的話,會像是這樣:
var tris = new ArrayList<List<Integer>>();
range(1, 11).forEach(a -> {
range(1, 11).forEach(b -> {
range(1, 11).forEach(c -> {
if(a <= b && b <= c && a * a + b * b == c * c) {
tris.add(asList(a, b, c));
}
});
});
});
程式碼變成著重在 List
要如何建立,儘管已經使用了 Lambda 相關功能,程式碼仍不見得好懂。
來舉另一個例子,〈阿姆斯壯數〉,其定義為在 n
位的整數中,加總每個數字的 n
次方後等於該整數,例如 153
是個阿姆斯壯數,因為 13 + 53 + 33 = 153
。
如果你的需求是,找出 3
位數的阿姆斯壯數有哪些,那你要定義(宣告)「什麼」是 3
位數的阿姆斯壯數,也就是「在 3
位的整數中,加總每個數字的 3
次方後等於該整數」。
在〈阿姆斯壯數〉,已經示範了一種定義方式,其中需要將 n
位整數分解為各數字,再判斷加總每個數字的 n
次方後等於該整數,這邊可以換另一個方向,首先定義 3
位的整數:
[x * 100 + y * 10 + z|
x <- [1..9], y <- [0..9], z <- [0..9]]
接下來,加入加總每個數字的 3
次方後等於該整數的定義:
[x * 100 + y * 10 + z|
x <- [1..9], y <- [0..9], z <- [0..9],
x ^ 3 + y ^ 3 + z ^ 3 == x * 100 + y * 10 + z]
List Comprehension 中可以使用 let
,例如,可以令 x * 100 + y * 10 + z
為 number
,這樣就不用重複計算這個算式了:
[number|
x <- [1..9], y <- [0..9], z <- [0..9],
let number = x * 100 + y * 10 + z,
x ^ 3 + y ^ 3 + z ^ 3 == number]
跟命令式比較一下,例如 Java 的實作,哪個比較清楚呢?
var numbers = new ArrayList<Integer>();
range(1, 10).forEach(x -> {
range(0, 10).forEach(y -> {
range(0, 10).forEach(z -> {
var number = x * 100 + y * 10 + z;
if(x * x * x + y * y * y + z * z * z == number) {
numbers.add(number);
}
});
});
});