使 using 更適合 FP 操作

C# 有個著名的 using statement,對於實踐 IDisposable 的物件特別好用,但 using 是個 statement,在 Imperative 世界沒問題,但在 Functional 世界,statement 就類似 句點,讓我們無法繼續 Pipeline 或對其他 function 做 Compose,我們能否比照將 foreach statement 重構成 ForEach() function,也將 using statement 重構成 using() function 呢 ?

Version


macOS High Sierra 10.13.6
.NET Core 2.1
C# 7.2
F# 4.5
Rider 2018.1.4

C# 之 Using Statement


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using System.IO;

namespace ConsoleApp
{
public static class Program
{
public static void Main()
{

using (var streamReader = new StreamReader("TestFile.txt"))
{
var line = streamReader.ReadToEnd();
Console.WriteLine(line);
// Hello World
}
}
}
}

StreamReader 是個典型實踐 IDisposable 的物件,所以在使用時都會使用 using statement 包起來,等離開 {} scope 時,自動呼叫 Dispose() 釋放 resource。

這些都是我們都習慣的 C#。

using 是 statement,在 Imperative 世界沒問題,反正程式碼都是一行一行循序執行。

但在 Functional 世界,我們要求 code 要 Pipeline,要 Compose,所以 FP 喜歡使用 expression,不喜歡 statement。

Statemet 就類似 句點,讓所有的 Pipeline 都中斷了。

其實仔細看 using statement,其實包含幾個部分:

  • Setup : 獲得 resource
  • Body : 執行 resource
  • Teardown : 釋放 resource

其中 using statement 就是幫我們做 teardown 部分。

因此我們可以自己寫一個 Using() function,將 setup 與 body 傳入 Using()

C# 之 Using() Function


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System.IO;
using static Functional.F;

namespace ConsoleApp
{
public static class Program
{
public static void Main()
{

Using(new StreamReader("TestFile.txt"), ReadFile)
.WriteLine();

string ReadFile(StreamReader streamReader) => streamReader.ReadToEnd();
}
}
}

10 行

1
2
Using(new StreamReader("TestFile.txt"), ReadFile)
.WriteLine();

使用 Using() function,將 setup 傳入第一個參數,將 body 傳入第二個參數。

由於 ReadFile() 回傳為 string,因此 Using() 也是回傳 string,這樣就可以使用 Pipeline 方式 WriteLine() 直接印出。

13 行

1
string ReadFile(StreamReader streamReader) => streamReader.ReadToEnd();

Body 以 local function 定義。

至於 Using()WriteLine() 怎麼來的呢 ? 是我們自己寫的 Higher Order Function。

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

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

public static void WriteLine(this string data)
{

Console.WriteLine(data);
}
}
}

第 7 行

1
2
3
4
public static R Using<TDisp, R>(TDisp disposable, Func<TDisp, R> f) where TDisp : IDisposable
{
using (disposable) return f(disposable);
}

自己寫一個 Using() HOF,第一個參數傳入 IDisposable 物件,第二個參數傳入 body function。

12 行

1
2
3
4
public static void WriteLine(this string data)
{

Console.WriteLine(data);
}

自己為 string 加上 WriteLine() Extension Method,這就就可以對 string 繼續 Pipeline 印出。

C# 為了讓 using 用起來更 FP,我們必須自己實作 Using()WriteLine(),但在 Functional First 的 F#,除了提供 Imperative 的 use 外,也提供了 Functional 的 using(),我們完全不用自己另外實作

F# 之 Use Bind


1
2
3
4
5
6
7
8
open System.IO

let readFromFile (fileName: string) =
use streamReader = new StreamReader(fileName)
streamReader.ReadToEnd()

readFromFile "TestFile.txt"
|> printf "%A"

F# 之 use 類似於 let,差別是 use 在離開 function 就會呼叫 Dispose(),不需特別加上 {} 縮排一層。

由於 readFromFile() 回傳 string,可以直接 Pipeline 接內建的 printf()

use 仍然是個 statement。

F# 之 Using() Function


1
2
3
4
5
6
7
8
9
10
open System.IO

let readFile (streamReader: StreamReader) =
streamReader.ReadToEnd()

let readFromFile (fileName: string) =
using(new StreamReader(fileName)) readFile

readFromFile "TestFile.txt"
|> printf "%A"

第 6 行

1
2
let readFromFile (fileName: string) =
using(new StreamReader(fileName)) readFile

改用 F# 內建的 using(),第一個參數傳入 IDisposable 物件,第二個參數傳入 body function,其實跟自己用 C# 實作的 Using() 是一樣的。

第 3 行

1
2
let readFile (streamReader: StreamReader) =
streamReader.ReadToEnd()

定義 body function。

由於 using()printf() 都是 F# 內建,因此我們就不必再自己實作了

Conclusion


  • 將 C# 由 using statement 改成 using() function,乍看之下意義不大;但若去看 F# 同時提供 use statement 與 using() function 時,就可看出 F# 的用心良苦,同時支援了 Imperative 與 Functional 兩種 paradigm
  • 由於 F# 每個 function 都是 composable,因此我們就不必再自已寫 WriteLine() 了,直接 printf() 就可以 pipeline 起來

Sample Code


  • C# : 完整的範例可以在我的 GitHub 上找到
  • F# : 完整的範例可以在我的 GitHub 上找到

Reference


Enrico Buonanno, Functional Programming in C#

2018-08-21