FP 之 Higher Order Function
Higher Order Function (HOF) 可以說是 FP 的精華,就算是目前主流 OOP,也大都接受了 HOF 概念,但實務上要活用 HOF 並不容易,需要時間與訓練,本文整理出實務上最常使用 HOF 的 4 種 Pattern,讓大家更容易運用在日常開發中。
Version
C# 7.2
本文為 Funtional Programming in C# 一書第一章的讀後心得
Definition
Higher Order Function
- 以 function 作為 function 的 input
- 以 function 作為 function 的 outpt
- 符合以上其中之一條件就算 Higer Order Function,簡稱 HOF
在 C# 中,最典型的 HOF 就是 LINQ,如最常用的 Select()
、Where()
就是 HOF。
本文將以 HOF 稱呼 Higher Order Function
實務上的應用
HOF 在實務上可歸納出以下 4 種 Pattern:
Inversion of Control
Adapter Function
Function Factory
To Avoid Duplication
Inversion of Control
Inversion of Control
原本由高階模組決定
控制流程
,改成由低階模組
決定控制流程
,高階模組只決定實作部分
可將
控制流程
寫成 Library 或 Framework,實現 Separation of Concerns (關注點分離):低階模組關注於控制流程
,而高階模組專心於實作部分
HOF 目的在實現 Inversion of Control
以 LINQ 的 Where()
為例 (相當於 FP 的 Filter()
)
1 | public static IEnumerable<T> Where<T>(this IEnumerable<T> data, Func<T, bool> predicate) |
低階模組 LINQ 的 Where
決定了整個 控制流程
,包含 foreach
與 if
, 高階模組只決定 predicate 的 實作部分
,這就是 Inversion of Control。
1 | class Cache<T> where T : class |
若可由 Guid 對 Cache
抓資料,若有資料則從 Cache 傳回,若沒資料則執行高階模組提供的 function。
我們可發現低階模組 Cache
決定 控制流程
,高階模組則提供 onMiss
function 的實作,可能是複雜的演算法計算,也可能是實際從資料庫抓資料。
HOF 最常使用的場景就是為了實現 Inversion of Control。
IoC 與 DIP (Dependency Inversion Principle 依賴反轉原則) 並不一樣,IoC 強調的是
控制流程
的反轉,而 DIP 強調的是藉由 interface 達到依賴
的反轉
Adapter Function
Adapter Function
HOF 的目的在於改變 function 的 Signature
1 | int divide(int x, int y) => x / y; |
原本 divide()
的 被除數
是 x
,除數
是 y
。
因為需求改變,被除數
改成 y
,而 除數
改成 x
,也就是 Signature 會改變,argument 會對調。
當然可以直接修改 code,基於 開放封閉原則
,且這也是常見的需求,決定將此功能 一般化
,將寫一個 function 來處理。
1 | static Func<T2, T1, R> SwapArgs<T1, T2, R>(this Func<T1, T2, R> f) |
SwapArgs()
回傳一個新的 function,其 argument 由原本的 (t1, t2)
改成 (t2, t1)
。
1 | var divideBy = divide.SwapArgs(); |
- 在 OOP 中,若 Interface 不同,我們會使用 Adapter Pattern,將 interface 加以轉換
- 在 FP 中,Function Signature 就是 Interface,若 Signature 不同,我們可使用 HOF 加以轉換,也稱為 Adapter Function
Function Factory
Function Factory
HOF 的目的就是建立新的 function
1 | var data = Enumerable.Range(1, 10) |
目前只能找出 偶數
,也就是 除以 2
整除。
若我們想讓功能更 一般化
,能找出 除以 n
整除的資料。
1 | Func<int, bool> isMod(int n) => x => x % n == 0; |
isMod()
HOF 不只更 一般化
,可讀性
也更高。
isMod()
HOF 目的並不是回傳 data,而是回傳 Where()
所需要的 function。
To Avoid Duplication
To Avoid Duplication
HOF 的目的在抽出程式碼共用部分
1 | int Foo1(Func<int, int> f1, ...) |
實務上常會發現不同 function,前面 setup 部分都相同,最後 teardown 部分也相同,只有中間 body 部分不同,這種時機就很適合使用 HOF,將共用部分抽出來。
將 Setup / Teardown 抽成共用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24using Dapper;
public class DbLogger
{
string connString;
public void CreateLog(LogMessage logMessage)
{
using (var conn = new SqlConnection(connString))
{
conn.Open();
conn.Execute("sp_create_log", logMessage, CommandType.StoredProcedure);
}
}
public IEnumerable<LogMessage> GetLogs(DateTime since)
{
using (var conn = new SqlConnection(connString))
{
conn.Open();
conn.Query<LogMessage>(@"SELECT * FROM [Logs] WHERE [Timestamp] > @since", new {since = since});
}
}
}
我們可以發現 CreateLog()
與 GetLogs()
在 using
部份有重複,因此可以建立 HOF 將共用部分抽出來。
1 | using System; |
建立 ConnectionHelper.Connect()
HOF,將 CreateLog()
與 GetLogs()
共用部分抽出來。
1 | using Dapper; |
抽出共用到 ConnectionHelper
之後,DbLogger
就不再有程式碼重複的部分。
實務上常將程式碼中 setup 與 teardown 部分抽成 HOF 共用
將 using 重構成 HOF1
2
3
4
5
6
7
8
9
10
11
12
13
14
15using System;
using System.Data;
using System.Data.SqlClient;
public class static class ConnectionHelper
{
public static R Connect<R>(string connString, Func<IDbConnection, R> f)
{
using (var conn = new SqlConnection(connString))
{
conn.Open();
return f(conn);
}
}
}
using
為 C# 內建的 statement,其實仔細一看,using
也是在做 setup 與 teardown 的事情:
- Setup : 獲得
IDisposable
resource - Body : 執行
{}
內的程式碼 - Teardown:呼叫
Dispose()
釋放 resource
我們可以也可以比照將 foreach
statement 重構成 ForEach()
function,將 using
statement 重構成 Using()
function。
1 | using System; |
將 Using()
建立在自己的 Functional Library 內。
1 | using static LaYumba.Functional.F; |
將 using
由 statement 重構成 Using()
function 後,有幾個優點 :
Connect()
程式碼更加簡潔,可以使用 Expression BodyUsing()
是 function,不是 statement,因此能夠再與其他 function 作 compose
HOF 的優點與缺點
優點
- Conciseness : 使用 function 後,能夠再與其他 function 作 compose,幾乎都是一行就能解決,這也是為什麼 C# 要全面提供 Expression Body
- Avoid Duplication : Setup 與 teardown 的邏輯不再重複
- Separation of Concerns :
ConnectionHelper
關注 connection 管理;而DbLogger
關注於 log 相關邏輯
缺點
- HOF 會使得 call stack 增加,可能會對效能有所影響,不過這是 CPU 層級,差異只是在幾個 clock cycle,所以可以忽略不計
- 由於 call stack 的增加,debug 會比較複雜
不過 HOF 所帶給我們的優點,仍然是一個值得投資 trade off。
Conclusion
- HOF 已經是算是目前所有程式語言都能接受的觀念,儘管是 OOP,也都能夠接受 HOF
- 過度使用 HOF 反而會使得 code 過度抽象化而難以理解,記得要以
可讀性
為前提,適當地使用 HOF - HOF 雖然可能造成 call stack 增加而難以 debug,但 HOF 所帶來的優點更多,仍然值得投資
Reference
Enrico Buonanno, Functional Programming in C#