深入探討 FP 之 Higher Order Funtion

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

内容简介: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#


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

查看所有标签

猜你喜欢:

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

Distributed Algorithms

Distributed Algorithms

Wan Fokkink / The MIT Press / 2013-12-6 / USD 40.00

This book offers students and researchers a guide to distributed algorithms that emphasizes examples and exercises rather than the intricacies of mathematical models. It avoids mathematical argumentat......一起来看看 《Distributed Algorithms》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

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

在线 XML 格式化压缩工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具