千年傳統,全新感受

F# 並不是一個新語言,早在 2005 年就已經發行 1.0,隨著 .NET Core 的跨平台,也將 F# 帶進了 .NET Core,既然在 .NET Core 我們已經有了 C#,為什麼要關注 F# 呢 ?

Version


macOS High Sierra 10.13.3
.NET Core 2.1.4
F# 4.1

FSharp 的歷史


F# 並不像 C-style 語言,反而比較像 Python,事實上 F# 是從 ML、OCaml、Python、Haskell、Scala、Erlang … 等語言獲得靈感,除了 Python 大家較為熟悉外,剩下的都是 Functional Programming Language,由此可見 F# 的 F 就是 Funtional,所以 F# 號稱是 function first language,也就是 F# 雖然也支援 OOP,但 FP 是其主要特色。

個人大概在 2010 年曾經接觸過 F#,當初的感覺 F# 是個 外星語言,很難體會 F# 的優點在哪裡,學沒多久就放棄了,但經過這幾年 Laravel Collection、JavaScript 、Linq 、Rx.js 與 AWS Lambda 的轟炸,越來越覺得 FP 的可愛,OOP 也能藉由 FP 手法,產生出現更優雅的實作方式。

這幾年一直想尋找一個 FP 語言來練習,期間摸過 Elixir 、Scala、Clojure,但成效一直有限,一直到最近複習 F#,才發現 F# 是一個很簡單的 FP 語言,以前看不懂的地方,瞬間都看懂了,只是當年還無法欣賞 F#。

Functional Programming 定義


一個語言要能實現 FP,必須有 4 個條件:

  • 能將 function 指定為變數
  • 能將 function 存到 collection 內
  • 能將 function 以參數型式傳入 function
  • 能在 function 回傳 function

簡單來說,function 要能如一般變數與 object 一樣使用。

能將 function 以參數型式傳入 function 導致了 Higher Order Function 的觀念出現,如 Rx.js 一堆 operator,就是 Higher Order Function。

能在 function 回傳 function 則導致了 Pipeline、Compose 與 Currying 的觀念出現。

一般 OOP 語言都會某種程度的支援 FP,如 Higher Order Function 在 C#、JavaScript、PHP … 都可以實現。

但 Pipeline、Compose 與 Currying 在一般 OOP 語言則沒有,或者要另外安裝其他 package 才能實現,但這些觀念在 F# 都是原生支援。

User Story


我們想將陣列 1, 2, 3, 4, 5 的資料中,將所 奇數 平方再加 1

Task


根據 JavaScript、Linq 、Rx.js 的經驗,我們不再使用迴圈,而會使用 Higher Order Function 來解決問題。

Higher Order Function


Program.fs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
open System

[<EntryPoint>]
let main argv =
let numbers = [1; 2; 3; 4; 5]

let isOdd x = x % 2 <> 0
let square x = x * x
let addOne x = x + 1

let func values =
let odds = List.filter isOdd values
let squares = List.map square odds
let result = List.map addOne squares
result

printfn "%A" (func numbers)

0

12 行

1
let odds = List.filter isOdd values

使用 List.filter 先找出所有 奇數

  • isOdd : 傳入 判斷奇數 的 function
  • values : 傳入欲處理資料
  • odds : 回傳所有 奇數

第 7 行

1
let isOdd x = x % 2 <> 0

定義 isOdd,當 % 2 餘數不為 0 時為 奇數

在 F#,因為已經將 function 視為一般變數,所以無論是 value 或 function,都統一使用 let

13 行

1
let squares = List.map square odds

既然已經找出所有 奇數,接下來就是使用 List.map 計算 平方

  • square : 傳入 計算平方 的 function
  • odds : 傳入所有奇數
  • squares : 回傳所有 平方

第 8 行

1
let square x = x * x

定義 square,計算平方。

14 行

1
let result = List.map addOne squares

既然已經計算出所有 奇數的平方,接下來就是使用 List.map 計算 +1

  • square : 傳入 計算+1 的 function
  • odds : 傳入所有奇數的平方
  • squares : 回傳所有 平方+1

第 9 行

1
let addOne x = x + 1

定義 addOne,計算 +1

15 行

1
result

要回傳的變數, F# 不用寫 return

相對於 C-style 語言,我們發現 F# 有幾個特色

  1. 沒有 {},完全用縮排表示,類似 Python
  2. Function 傳入參數不需 (),只需空白隔開即可
  3. 變數與 function 統一使用 let
  4. 回傳值不需要 return

這些只是語法的差異,只要習慣即可,不過平心而論,C-style 語言寫久,會發現 code 都一堆 () {}return 都是贅字,F# 這種 coding style 乾淨很多

Pipeline 與 Currying


由於 List.filterList.mapList.map 是依序處理,因此我們要不斷定義中繼變數 : oddssquares 傳入,事實上這些也是多餘的,若能省略則更好,這就是 Pipeline。

Program.fs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
open System

[<EntryPoint>]
let main argv =
let numbers = [1; 2; 3; 4; 5]

let isOdd x = x % 2 <> 0
let square x = x * x
let addOne x = x + 1

let func values =
values
|> List.filter isOdd
|> List.map square
|> List.map addOne

printfn "%A" (func numbers)

0

12 行

1
2
3
4
values 
|> List.filter isOdd
|> List.map square
|> List.map addOne

|> 為 F# 的 Pipeline 符號,表示將 function 的 output 作為下一個 function 的 input。

因此我們可以透過 |> 表示先執行 List.flter,然後再將結果傳入 List.map,最後再將結果傳入 List.map,這樣可以很清楚的表示流程,語意比 imperative 寫法更清楚。

Q : 可以明明 List.mapList.filter 是 2 個參數,第 1 個參數是 function,第 2 個參數是 value,但為什麼 value 都不用傳呢 ?

當 function 參數沒有傳完全時,F# 將回傳一個新的 function,新的 function 只要傳入剩下的參數即可,這稱為 Currying。

List.filter isOdd 只傳入 1 個參數時,由於參數沒有傳完整,將回傳一個新的 function,然後 |> 再將 values 傳入新的 function,如此 List.filter 才算完整,才能回傳 所有奇數,最後再將 所有奇數透過 |> 傳給下一個 List.map,剩下以此類推。

Compose


Pipeline 雖然已經夠清楚,但 pipeline 基本上仍然是回傳 value,若我們能將所有 function 先組合好,最後統一透過一個 function 執行,那就更好了,這就是 Compose。

Program.fs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
open System

[<EntryPoint>]
let main argv =
let numbers = [1; 2; 3; 4; 5]

let isOdd x = x % 2 <> 0
let square x = x * x
let addOne x = x + 1
let sqaureAddOne = square >> addOne

let func = List.filter isOdd >> List.map sqaureAddOne

printfn "%A" (func numbers)

0

12 行

1
let func = List.filter isOdd >> List.map sqaureAddOne

>> 為 F# 的 compose 符號,專門負責組合 function。

因為連續兩個 List.map,因此我們先將 square 與 addOne 組合成新的 sqaureAddOne function,再交給 List.map 執行。

由於是先執行 List.fiter ,再執行 List.map,因此使用 List.filter >> List.map

>> 不只代表 compose,也代表執行方向,所以也有 <<

Q&A


Q : 學 F# 目前能做什麼 ?

  1. F# 可跑在 .NET Core,所以能用 F# 寫 ASP.NET MVC、ASP.NET Web API、UWP 與 Console App
  2. F# 可用來寫 Azure Function

Conclusion


  • 本文簡單的展示 F# 最關鍵的 Pipeline、Currying 與 Compose,這些都是 OOP 語言很難見到的強悍功能,透過 F# 的簡單實作,讓我們在練習 FP 時更加方便
  • F# 並不是要取代 C#,事實上在 .NET Core,C# 仍是必學的語言,只是透過學習 F#,能訓練自己 FP 的思維,進而用在 C# 與 TypeScript 上

  • 若語言間的 paradigm 相同,只是 syntax 不同,則沒有學習新語言的必要;但若透過更好的 syntax,讓你學到不同的 paradigm,這就有意義了,這就是學習 F# 的原因

Sample Code


完整的範例可以在我的 GitHub 上找到

Reference


Microsoft Docs, Tour of F#

2018-03-01