FP 的基本功

F# 的 List module 提供眾多 List 常用的 Higher Order Function,要能發揮 FP 的威力,首先必須能活用內建 function。

本文探討英文字母 G ~ P 開頭的 function。

Version


macOS High Sierra 10.13.3
.NET Core SDK 2.1.101
Rider 2017.3.1
F# 4.1

List.head


傳回 list 第一個 element,相當於 list 的 Head property

list : 'T list -> 'T

1
2
3
4
[ 1; 2; 3 ]
|> List.head
|> printf "%d"
// 1

若為 empty list,會拋出 ArgumentException

Q:List.head()List.tryHead() 有何差異?

相同

  • 都傳回 list 第一個 element

相異

  • List.head() 傳回 'T;而 List.tryHead() 傳回 'T option

  • 若找不到資料,List.head() 會拋出 ArgumentException;但 List.tryHead() 只會回傳 None

實務上建議使用 List.tryHead() 取代 List.head(),較為安全

List.indexed


將 element 的 key 與 value 封裝成 tuple 的 list

list : 'T list -> (int * 'T) list

1
2
3
4
[ 1; 2; 3 ]
|> List.indexed
|> printf "%A"
// [(0, 1); (1, 2); (2, 3)]

List.init


根據 index 值建立新的 list

int -> initializer : (int -> 'T) -> 'T list

1
2
3
List.init 10 (fun idx -> idx * 2)
|> printf "%A"
// [0; 2; 4; 6; 8; 10; 12; 14; 16; 18]

List.isEmpty


判斷 list 是否為空,相當於 list 的 IsEmpty property

list : 'T list -> bool

1
2
3
4
[]
|> List.isEmpty
|> printf "%b"
// true

List.item


傳回 list 指定 index 的 element 值,相當於 list 的 Item property

index : int -> list : 'T list -> 'T

1
2
3
4
[ 1; 2; 3 ]
|> List.item 2
|> printf "%d"
// 3

若找不到 element,會拋出 ArgumentException

Q:List.item()List.tryItem() 有何差異?

相同

  • 都傳回 list 指定 index 的 element 值

相異

  • List.item() 傳回 'T;而 List.tryItem() 傳回 'T option

  • 若找不到資料,List.head() 會拋出 ArgumentException;但 List.tryHead() 只會回傳 None

實務上建議使用 List.tryItem() 取代 List.item(),較為安全

List.iter


對每個 element 執行回傳值為 unit 的 action function

action : ('T -> unit) -> list : 'T list -> unit

1
2
3
[ 1; 2; 3 ]
|> List.iter (printf "%A ")
// 1 2 3

List.iter2


同時對兩個 list 的每個 element 執行回傳值為 unit 的 action function

action : ('T1 -> 'T2 -> unit) -> list1 : 'T1 list -> list2 : 'T2 list -> unit

1
2
3
4
5
6
7
8
let list1 = [ 1; 2; 3 ]
let list2 = [ 4; 5; 6 ]

let action elm1 elm2 =
printf "%d " (elm1 + elm2)

List.iter2 action list1 list2
// 5 7 9

List.iteri


對每個 element 執行回傳值為 unit 的 action function,但會將 index 也傳入 action function

action : (int -> 'T -> unit) -> list : 'T list -> unit

1
2
3
4
5
[ 1 .. 3 ]
|> List.iteri (fun idx elm -> printfn "%d : %d" idx elm)
// 0 : 1
// 1 : 2
// 2 : 3

List.iteri2


同時對兩個 list 的每個 element 執行回傳值為 unit 的 action function,但會將 index 也傳入 action function

action : (int -> 'T1 -> 'T2 -> unit) -> list1 : 'T1 list -> list2 : 'T2 list -> unit

1
2
3
4
5
6
7
8
9
10
let list1 = [ 1; 2; 3 ]
let list2 = [ 4; 5; 6 ]

let action idx elm1 elm2 =
printfn "%d : %d" idx (elm1 + elm2)

List.iteri2 action list1 list2
// 0 : 5
// 1 : 7
// 2 : 9

List.last


傳回 list 最後一個 element

list : 'T list -> 'T

1
2
3
4
[ 1; 2; 3 ]
|> List.last
|> printf "%d"
// 3

若找不到 element,會拋出 ArgumentException

Q:List.last()List.tryLast() 有何差異?

相同

  • 都傳回 list 最後一個 element 值

相異

  • List.last() 傳回 'T;而 List.tryLast() 傳回 'T option

  • 若找不到資料,List.last() 會拋出 ArgumentException;但 List.tryLast() 只會回傳 None

實務上建議使用 List.tryLast() 取代 List.last(),較為安全

List.length


傳回 list 的 element 個數,相當於 list 的 Length property

list: 'T list -> int

1
2
3
4
[ 1; 2; 3 ]
|> List.length
|> printf "%d"
// 3

List.map


將 list 的每個 element 根據 mapping function 轉換成另外一個 value 的 list

mapping : ('T -> 'U) -> list : 'T list -> 'U list

1
2
3
4
[ 1; 2; 3 ]
|> List.map (fun elm -> elm * elm)
|> printf "%A"
// [1; 4; 9]

若經過 mapping function 後 element 個數不一樣,請用 List.collect()

List.map2


將兩個 list 的每個 element 根據 mapping function 轉換成另外一個 value 的 list

mapping : ('T1 -> 'T2 -> 'U) -> list1 : 'T1 list -> list2 : 'T2 list -> 'U list

1
2
3
4
5
6
let list1 = [ 1; 2; 3 ]
let list2 = [ 4; 5; 6 ]

List.map2 (fun elem1 elem2 -> elem1 + elem2) list1 list2
|> printf "%A"
// [5; 7; 9]

List.map3


將三個 list 的每個 element 根據 mapping function 轉換成另外一個 value 的 list

mapping : ('T1 -> 'T2 -> 'T3 -> 'U) -> list1 : 'T1 list -> list2 : 'T2 list -> list3 : 'T3 list -> 'U list

1
2
3
4
5
6
7
let list1 = [ 1; 2; 3 ]
let list2 = [ 4; 5; 6 ]
let list3 = [ 7; 8; 9 ]

List.map3 (fun elm1 elm2 elm3 -> elm1 + elm2 + elm3) list1 list2 list3
|> printf "%A"
// [12; 15; 18]

List.mapFold


相當於 List.map() + List.fold() 合體,回傳為 tuple。

mapping : ('State -> 'T -> 'Result * 'State) -> state : 'State -> list : 'T list -> 'Result list * 'State

1
2
3
4
[ 1; 2; 3 ]
|> List.mapFold (fun acc elm -> (elm * elm , acc + elm * elm)) 0
|> printf "%A"
// ([1; 4; 9], 14)

mapping funciton 中,回傳的是 tuple。

  • 第 1 個 element 為 List.map() 的結果
  • 第 2 個 element 為 List.fold() 的結果

List.mapFoldBack


相當於 List.map() + List.fold() 合體,回傳為 tuple,但從 list 的尾部開始,signature 的順序也不一樣

mapping : ('T -> 'State -> 'Result * 'State) -> list : 'T list -> state : 'State -> 'Result * 'State

1
2
3
4
5
let list1 = [ 1; 2; 3 ]

List.mapFoldBack (fun elm acc -> (elm * elm , acc + elm * elm)) list1 0
|> printf "%A"
// ([1; 4; 9], 14)

List.mapi


對 list 的每個 element 根據 mapping function 轉換成另外一個 value 的 list,但會將 index 也傳入 mapping function

mapping : (int -> 'T -> 'U) -> list : 'T list -> 'U list

1
2
3
4
[ 1; 2; 3 ]
|> List.mapi (fun idx elm -> idx * elm)
|> printf "%A"
// [0; 2; 6]

List.mapi2


同時對兩個 list 的每個 element 根據 mapping function 轉換成另外一個 value 的 list,但會將 index 也傳入 mapping function

1
2
3
4
5
6
7
8
9
let list1 = [ 1; 2; 3 ]
let list2 = [ 4; 5; 6 ]

let mapping idx elm1 elm2 =
idx + elm1 + elm2

List.mapi2 mapping list1 list2
|> printf "%A"
// [5; 8; 11]

List.max


傳回 list 內 element 最大值

list : 'T list -> 'T

1
2
3
4
[ 1; 2; 3 ]
|> List.max
|> printf "%d"
// 3

List.maxBy


傳回 list 內 element 最大值,但所比較的是經過 projection function 轉換過的值

project : ('T -> 'U) -> list : 'T list -> 'T

1
2
3
4
[ 1; 2; 3 ]
|> List.maxBy (fun elm -> 1 - elm)
|> printf "%d"
// 1

[1; 2; 3] 經過 projection function 後為 [0; -1; -2],所以最大值為 0,但 List.maxBy() 回傳的是原 list 的值,所以為 1

Projection function 的值只是比較用,不是回傳用

List.min


傳回 list 內 element 最小值

list : 'T list -> 'T

1
2
3
4
[ 1; 2; 3 ]
|> List.min
|> printf "%d"
// 1

List.minBy


傳回 list 內 element 最小值,但所比較的是經過 projection function 轉換過的值

project : ('T -> 'U) -> list : 'T list -> 'T

1
2
3
4
[ 1; 2; 3 ]
|> List.minBy (fun elm -> 1 - elm)
|> printf "%d"
// 3

[1; 2; 3] 經過 projection function 後為 [0; -1; -2],所以最小值為 0,但 List.minBy() 回傳的是原 list 的值,所以為 3

Projection function 的值只是比較用,不是回傳用

List.ofArray


由 array 轉成 list

array : 'T [] -> 'T list

1
2
3
4
[| 1; 2; 3 |]
|> List.ofArray
|> printf "%A"
// [1; 2; 3]

List.ofSeq


由 sequence 轉成 list

source : seq<'T> -> 'T list

1
2
3
4
seq { 1 .. 5 }
|> List.ofSeq
|> printf "%A"
// [1; 2; 3; 4; 5]

List.pairWise


根據 list 順序,回傳兩兩成為 tuple 的 list

list : 'T list -> ('T * 'T) list

1
2
3
4
[ 1; 2; 3; 4 ]
|> List.pairwise
|> printf "%A"
//[(1, 2); (2, 3); (3, 4)]

List.partition


將 list 根據 predicate function 條件拆成兩個 list

predicate : ('T -> bool) -> list : 'T list -> 'T list * 'T list

1
2
3
4
[ 1 .. 10 ]
|> List.partition (fun elm -> elm % 2 = 0)
|> printf "%A"
// ([2; 4; 6; 8; 10], [1; 3; 5; 7; 9])

List.permute


List 重新根據 indexMap function 的規則,重新調整 element 順序

indexMap : (int -> int) -> list : 'T list -> 'T list

1
2
3
4
[ 1; 2; 3 ]
|> List.permute (fun idx -> (idx + 1) % 3)
|> printf "%A"
// [3; 1; 2]

List.pick


找出 list 中符合 chooser function 條件的第一個 element

chooser : ('T -> 'U option) -> list : 'T list -> -> 'U

1
2
3
4
5
6
7
8
9
let chooser elm = 
match elm with
| elm when elm % 2 = 0 -> Some elm
| _ -> None

[ 1; 2; 3 ]
|> List.pick chooser
|> printf "%A"
// 2

若找不到 element,會拋出 KeyNotFoundException

Q:List.find()List.pick() 有何差異?

相同

  • 都用來找資料

  • 找不到資料都會拋出 KeyNotFoundException

相異

  • List.find() 的 predicate function 為 'T -> bool
  • List.pick() 的 chooser function 為 'T -> 'U option

若 fuction 回傳為 option,則適合用 List.pick(),否則使用 List.find()

Conclusion


  • List module 所提供的 function 都必須非常熟練,因為這些都是常用的 Higher Order Function,算是 FP 的基本功

Reference


MSDN, Collection.List Module (F#)

2018-04-07