天天看點

#函數式程式設計 Functional Programming in C# [36]

7.6 将清單減少為單個值

  将一個值的清單縮減為一個單一的值是一個常見的操作,但我們到目前為止還沒有讨論過。在FP術語中,這種操作被稱為fold或reduce,這些是你在大多數語言或庫以及FP文獻中都會遇到的名稱。從字面上看,LINQ使用了一個不同的名字:Aggregate。 如果你已經熟悉了Aggregate,你可以跳過下一小節。

7.6.1 LINQ 的Aggregate方法

  請注意,到目前為止,我們用IEnumerable使用的大多數函數也會傳回一個IEnumerable。 例如,Map接收一個n個事物的清單,并傳回另一個n個事物的清單,可能是不同的類型。 Where和Bind也在抽象範圍内;也就是說,它們接收一個IEnumerable并傳回一個IEnumerable,盡管清單的大小或元素的類型可能不同。

  Aggregate與這些函數不同,它接收一個包含n個事物的清單,并準确地傳回一個事物(就像你可能熟悉的SQL聚合函數COUNT、SUM和AVERAGE)。

  給定一個 IEnumerable,Aggregate 接受一個名為 accumulator 的初始值和一個 reducer 函數——一個二進制函數接受accumulator和清單中的一個元素,并傳回accumulator的新值。 Aggregate 然後周遊清單,将函數應用于累加器的目前值和清單中的每個元素。

  例如,您可以列出檸檬并将其聚合成一杯檸檬汁。累加器将是一個空玻璃杯,如果檸檬清單是空的,這就是你得到的。 reducer 函數接受一個玻璃杯和一個檸檬,并傳回一個擠滿檸檬的玻璃杯。考慮到這些參數,Aggregate 周遊清單,将每個檸檬擠入玻璃杯中,最後将所有檸檬汁都歸還給玻璃杯。

  Aggregate的簽名是

(IEnumerable< T >, Acc, ((Acc, T) -> Acc)) -> Acc

  圖 7.3 以圖形方式顯示了它。如果清單為空,Aggregate 隻傳回給定的累加器 acc。如果它包含一項,t0,則傳回将f加到t0上的結果;我們稱這個值為 acc1。如果它包含更多項,它将計算acc1,然後将f應用于acc1和t1以獲得acc2,依此類推,最終傳回accN作為結果。 Acc 可以看作是一個初始值,使用給定的函數在其上應用清單中的所有值。

#函數式程式設計 Functional Programming in C# [36]

  Sum 函數(在 LINQ 中單獨可用)是 Aggregate 的一個特例。空清單中所有數字的總和是多少?自然是0!這就是我們的累加器值。二進制函數隻是加法,是以我們可以将 Sum 表示如下。

清單 7.16 Sum 作為 Aggregate 的特例

請注意,這将擴充為以下内容:

更一般地說,ts.Aggregate(acc,f)擴充為

Count 也可以看作是 Aggregate 的一個特例:

  請注意,累加器的類型不一定是清單項的類型。例如,假設我們有一個事物的清單,我們想把它們添加到Tree。我們清單中的類型是,比如說,T,而累加器的類型是Tree。我們可以用一個空的樹作為累加器開始,然後在周遊清單的過程中添加每一個項目。

清單7.17 使用Aggregate建立一個清單中所有項目的樹狀圖

  在這個例子中,我假設tree.Insert(i)傳回一個帶有新插入的值的樹。

  Aggregate 是一種非常強大的方法,它可以根據 Aggregate 實作 Map、Where 和 Bind——我建議将其作為練習。

  還有一個不太通用的重載,它不接受累加器參數,但使用清單的第一個元素作為累加器。此重載的簽名是

(IEnumerable< T >, ((T, T) -> T)) -> T

  使用此重載時,結果類型與清單中元素的類型相同,清單不能為空。

7.6.2 Aggregating 驗證結果

  既然您知道如何将值清單縮減為單個值,讓我們應用這些知識,看看我們如何将驗證器清單“縮減”為單個驗證器。為此,我們需要實作一個類型為

IEnumerable<Validator< T >> -> Validator< T >

  請注意,因為 Validator 本身是一個函數類型,是以前面的類型擴充為:

IEnumerable< T -> Validation< T >> -> T -> Validation< T >

  首先,我們需要決定我們希望組合驗證如何工作:

  • 快速失敗——如果驗證應該優化效率,一旦一個驗證器失敗,組合驗證就應該失敗,進而最大限度地減少資源的使用。如果您正在驗證從應用程式以程式設計方式發出的請求,這是一種很好的方法。
  • 收集錯誤——您可能希望識别所有被違反的規則,以便在發出另一個請求之前修複它們。在驗證使用者通過表單發出的請求時,這是一種更好的方法。

  失敗快速政策更容易實作:每個驗證器都會傳回一個Validation,而Validation暴露了一個Bind函數,隻有在狀态為Valid時才會應用綁定的函數(就像Option和Either),是以我們可以使用Aggregate來周遊驗證器清單,并将每個驗證器與運作結果綁定。

清單 7.18 使用 Aggregate 和 Bind 在序列中應用所有驗證
public static Validator < T > FailFast < T > 
	(IEnumerable < Validator < T >> validators) 
	=> t 
	=> validators.Aggregate(Valid(t),
		 (acc, validator) => acc.Bind(_ => validator(t)));
           

  請注意,FailFast函數接收一個Validators清單,并傳回一個Validator:一個期望對T類型的對象進行驗證的函數。在接收到驗證對象t後,它使用Valid(t)作為累加器周遊驗證器清單(也就是說,如果驗證器清單為空,那麼t就是有效的),并将清單中的每個驗證器用Bind應用到累加器上。

  從概念上講,對 Aggregate 的調用展開如下:

Valid(t)
	.Bind(validators[0]))
	.Bind(validators[1]))
	...
	.Bind(validators[n - 1]));
           

  由于Bind是為Validation定義的,隻要一個驗證器失敗了,後面的驗證器就會被跳過,而整個驗證就會失敗。

  并非所有的驗證都同樣昂貴。例如,用正規表達式來驗證BIC碼是否正确(如清單6.7所示)是非常便宜的。假設你還需要確定給定的BIC代碼是一個現有的銀行分行。這可能涉及到DB查詢或遠端調用一個具有有效代碼清單的服務,這顯然更昂貴。

  為確定整體驗證有效,您需要對驗證器清單進行相應排序。在這種情況下,您需要先應用(便宜的)正規表達式驗證,然後再應用(昂貴的)遠端查找。

7.6.3 收獲驗證錯誤

  相反的方法是優先考慮完整性;也就是說,要包括所有失敗的驗證的細節。在這種情況下,你不希望失敗阻止進一步的計算;相反,你想確定所有的驗證器都運作,并且所有的錯誤(如果有的話)都被收集了。

  例如,如果您正在驗證具有大量字段的表單,并且您希望使用者看到他們需要修複的所有内容以進行有效送出,那麼這很有用。

  讓我們看看如何重寫結合不同驗證器的方法。

清單 7.19 從所有失敗的驗證器中收集錯誤
public static Validator < T > HarvestErrors < T > 
	(IEnumerable < Validator < T >> validators) 
	=> t => 
{
    var errors = validators
    	.Map(validate => validate(t)) //獨立運作所有驗證器
    	.Bind(v => v.Match( 
    		Invalid: errs => Some(errs), //收集驗證錯誤
    		Valid: _ => None)).ToList(); //無視通過的驗證
    return errors.Count == 0 //如果沒有錯誤,則整體驗證通過。
    	? Valid(t) 
    	: Invalid(errors.Flatten());
};
           

  在這裡,我們沒有使用Aggregate,而是使用Map将驗證器的清單映射到要驗證的對象上運作驗證器的結果。這確定了所有的驗證器都被獨立調用,最後我們得到一個驗證器的IEnumerable。

  然後我們對收獲所有的錯誤感興趣。 要做到這一點,我們使用 Option:我們将 Invalids 映射到一個包裹錯誤的 Some,而 Valids 映射到 None。 還記得第4章嗎,Bind可以用來從一個Options清單中過濾出Nones,這就是我們在這裡要做的,以獲得一個所有錯誤的清單。因為每個Invalid都包含了一個錯誤清單,是以errors實際上是一個清單的清單。在失敗的情況下,我們需要把它平鋪成一個一維的清單,用它來填充一個Invalid。如果沒有錯誤,則傳回有效并輸入有效。

總結

  • 部分應用意味着為函數提供零散的參數,有效地為每個給定的參數建立一個更專業的函數。
  • 柯裡化意味着更改函數的簽名,以便一次接受一個參數。
  • 部分應用程式使您能夠通過參數化它們的行為來編寫高度通用的函數,然後提供參數以獲得越來越專業化的函數。
  • 參數的順序很重要:首先給出最左邊的參數,以便函數應該從一般到特定聲明它的參數。
  • 在 C# 中使用多參數函數時,方法解析可能會出現問題并導緻文法開銷。這可以通過依靠 Funcs 而不是方法來克服。
  • 您可以通過将函數聲明為參數來注入函數所需的依賴項。這允許您完全由函數組成您的應用程式,而不會影響關注點分離、解耦和可測試性。