深入探討 FP 之 Higher Order Funtion

栏目: C# · 发布时间: 5年前

内容简介:Higher Order Function (HOF) 可以說是 FP 的精華,就算是目前主流 OOP,也大都接受了 HOF 概念,但實務上要活用 HOF 並不容易,需要時間與訓練,本文整理出實務上最常使用 HOF 的 4 種 Pattern,讓大家更容易運用在日常開發中。C# 7.2本文為

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() )

public static IEnumerable<T> Where<T>(this IEnumerable<T> data, Func<T, bool> predicate)
{
    foreach(T iter in data)
    {
        if (predicate(iter))
        {
            yield return iter;
        }
    }
}

低階模組 LINQ 的 Where 決定了整個 控制流程 ,包含 foreachif , 高階模組只決定 predicate 的 實作部分 ,這就是 Inversion of Control。

深入探討 FP 之 Higher Order Funtion

class Cache<T> where T : class
{
    public T Get(Guid id) => ...
        
    public T Get(Guid id, Func<T> onMiss) => Get(id) ?? onMiss();
}

若可由 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

int divide(int x, int y) => x / y;
var result = divide(10, 2); // 5

原本 divide()被除數x除數y

因為需求改變, 被除數 改成 y ,而 除數 改成 x ,也就是 Signature 會改變,argument 會對調。

當然可以直接修改 code,基於 開放封閉原則 ,且這也是常見的需求,決定將此功能 一般化 ,將寫一個 function 來處理。

static Func<T2, T1, R> SwapArgs<T1, T2, R>(this Func<T1, T2, R> f)
    => (t2, t1) => f(t1, t2);

SwapArgs() 回傳一個新的 function,其 argument 由原本的 (t1, t2) 改成 (t2, t1)

var divideBy = divide.SwapArgs();
var result = divideBy(2, 10); // 5
  • 在 OOP 中,若 Interface 不同,我們會使用 Adapter Pattern,將 interface 加以轉換
  • 在 FP 中,Function Signature 就是 Interface,若 Signature 不同,我們可使用 HOF 加以轉換,也稱為 Adapter Function

Function Factory

Function Factory

HOF 的目的就是建立新的 function

var data = Enumerable.Range(1, 10)
                     .Where(x => x % 2 == 0);
// [2, 4, 6, 8, 10]

目前只能找出 偶數 ,也就是 除以 2 整除。

若我們想讓功能更 一般化 ,能找出 除以 n 整除的資料。

Func<int, bool> isMod(int n) => x => x % n == 0;
var data1 = Enumerable.Range(1, 10).Where(isMod(2)); // 2, 4, 6, 8, 10
var data2 = Enumerable.Range(1, 10).Where(isMod(3)); // 3, 6, 9

isMod() HOF 不只更 一般化可讀性 也更高。

isMod() HOF 目的並不是回傳 data,而是回傳 Where() 所需要的 function。

深入探討 FP 之 Higher Order Funtion

To Avoid Duplication

To Avoid Duplication

HOF 的目的在避免程式碼重複部分

int Foo1(Func<int, int> f1, ...)
{
    ...
    var x = f1(...);
    ...
}

int Foo2(Func<int, int> f1, ...)
{
    ...
    var x = f2(...);
    ...
}

實務上常會發現不同 function,前面 setup 部分都相同,最後 teardown 部分也相同,只有中間 body 部分不同,這種時機就很適合使用 HOF,將共用部分抽出來。

將 Setup / Teardown 抽成共用

using 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 將共用部分抽出來。

using 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);
        }
    }
}

建立 ConnectionHelper.Connect() HOF,將 CreateLog()GetLogs() 共用部分抽出來。

using Dapper;
using static ConnectionHelper;

public class DbLogger
{
    string connString;
    
    public void CreateLog(LogMessage logMessage)
        => Connect(connString, c => c.Execute("sp_create_log", logMessage, CommandType.StoredProcedure));
    
    public IEnumerable<LogMessage> GetLogs(DateTime since)
        => Connect(connString, c => c.Query<LogMessage>(@"SELECT * FROM [Logs] WHERE [Timestamp] > @since", new {since = since}))
}

抽出共用到 ConnectionHelper 之後, DbLogger 就不再有程式碼重複的部分。

實務上常將程式碼中 setup 與 teardown 部分抽成 HOF 共用

將 using 重構成 HOF

using 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。

using System;

namespace LaYumba.Functional
{
	public static class F
    {
        public static R Using<TDisp, R>(TDisp disposable, Func<TDisp, R> f) where TDisp : IDisposable
        {
            using(disposable) return f(disposable);
        }
    }
}

Using() 建立在自己的 Functional Library 內。

using static LaYumba.Functional.F;

public static class ConnectionHelper
{
    public static R Connect<R>(string connString, Func<IDbConnection, R> f)
        => Using(new SqlConnection(connStr), conn => { conn.Open(); return f(conn); });
}

using 由 statement 重構成 Using() function 後,有幾個優點 :

Connect()
Using()

HOF 的優點與缺點

優點

  • Conciseness : 使用 function 後,能夠再與其他 function 作 compose,幾乎都是一行就能解決,這也是為什麼 C# 要全面提供 Expression Body
  • Avoid Duplication : Setup 與 teardown 的邏輯不再重複
  • Sepration of Concerns : ConnectionHelper 關注 connection 管理;而 DbLogger 關注於 log 相關邏輯

缺點

深入探討 FP 之 Higher Order Funtion
  • HOF 會使得 call stack 增加,可能會對效能有所影響,不過這是 CPU 層級,差異只是在幾個 clock cycle,所以可以忽略不計
  • 由於 call stack 的增加,debug 會比較複雜

不過 HOF 所帶給我們的優點,仍然是一個值得投資 trade off。

Conclusion

可讀性

Reference

Enrico Buonanno, Functional Programming in C#


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

常用算法深入学习实录

常用算法深入学习实录

张子言 / 电子工业出版社 / 2013-10 / 89.00元

对于任何一门编程语言来说,算法都是程序的“灵魂”。正是因为算法如此重要,所以笔者精心编写了本书,希望通过书中的内容引领广大读者一起探讨学习算法的奥秘,带领广大读者真正步入程序开发的高级世界。 本书共分15章,循序渐进、由浅入深地详细讲解算法的核心内容,并通过具体实例的实现过程演练各个知识点的具体用法。本书首先详细讲解算法的基础知识,剖析了将算法称为“程序灵魂”的原因。然后详细讲解算法技术的核......一起来看看 《常用算法深入学习实录》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

MD5 加密
MD5 加密

MD5 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具