以重構角度探討 LINQ

LINQ 是 C# 3.0 實現 FP 重要里程碑,提供大量的 Operator,讓我們以 Pure Function 將 data 以 Dataflow 與 Pipeline 方式實現。本系列將先以 Imperative 實作,然後再重構成 FP,最後再重構成 LINQ Operator。

本文將討論 Select Operator。

Version


macOS High Sierra 10.13.6
.NET Core 2.1
C# 7.2
Rider 2018.2.3

Imperative


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{

var data = Enumerable.Range(1, 3);

var result = new List<int>();

foreach (var item in data)
{
result.Add(item * 2);
}

foreach (var item in result)
{
Console.WriteLine(item);
}
}
}
}

使用 Enumerable.Range() 產生 1, 2, 3,建立暫存的 result List,將所有元素都乘以 2,最後使用 foreach() 印出每個值。

由於資料的改變,建立新的暫存 List 處理,是 Imperative 慣用手法。

Refactor to HOF

實務上這種建立新暫存 List 處理的作法,常常會遇到,若每次都使用 foreach 這種 statement 寫法,重複使用能力為 0,就每次都要不斷的寫 foreach

若我們能將這種 foreach 配合新暫存 List 處理做法,抽成 MyMap() Higher Order Function,我們就能不斷 reuse MyMap(),只要將不同的商業邏輯以 function 傳進 MyMap() 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{

var data = Enumerable.Range(1, 3);

var result = MyMap(data, Double);

foreach (var item in result)
{
Console.WriteLine(item);
}

int Double(int x) => x * 2;
}

private static IEnumerable<int> MyMap(IEnumerable<int> data, Func<int, int> func)
{

var result = new List<int>();

foreach (var item in data)
{
result.Add(func(item));
}

return result;
}
}
}

23 行

1
2
3
4
5
6
7
8
9
10
11
private static IEnumerable<int> MyMap(IEnumerable<int> data, Func<int, int> func)
{

var result = new List<int>();

foreach (var item in data)
{
result.Add(func(item));
}

return result;
}

自己以 MyMap() 實作出 foreach statement + 暫存 List 處理的 Higher Order Function 版本。

第一個參數為 data,第二個參數為 function。

如此 MyMap() function 就能被重複使用。

13 行

1
var result = MyMap(data, Double);

原來的 foreach() statement 重構成 MyMap() Higher Order Function,將 data 與 Double Local Function 傳入即可。

Refactor to Yield Return


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{

var data = Enumerable.Range(1, 3);

var result = MyMap(data, Double);

foreach (var item in result)
{
Console.WriteLine(item);
}

int Double(int x) => x * 2;
}

private static IEnumerable<int> MyMap(IEnumerable<int> data, Func<int, int> func)
{

foreach (var item in data)
{
yield return func(item);
}
}
}
}

23 行

1
2
3
4
5
6
7
private static IEnumerable<int> MyMap(IEnumerable<int> data, Func<int, int> func)
{

foreach (var item in data)
{
yield return func(item);
}
}
  • 但要建立暫存 List 會影響執行效率,也浪費記憶體,尤其暫存 List 只是中繼資料,並不是最後執行結果

  • MyMap() 還要重新執行一次 foreach loop,也會影響執行效率

因此改用 yield return 實現 Lazy Evaluation,直到真正需要結果時,才會執行 func(item),如此就不用建立暫存 List,既能解省記憶體,又能減少一次 foreach loop 執行,能大幅增進執行效率。

Refactor to Generics


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{

var data = Enumerable.Range(1, 3);

var result = MyMap(data, Double);

foreach (var item in result)
{
Console.WriteLine(item);
}

int Double(int x) => x * 2;
}

private static IEnumerable<R> MyMap<T, R>(IEnumerable<T> data, Func<T, R> func)
{
foreach (var item in data)
{
yield return func(item);
}
}
}
}

23 行

1
2
3
4
5
6
7
private static IEnumerable<R> MyMap<T, R>(IEnumerable<T> data, Func<T, R> func)
{
foreach (var item in data)
{
yield return func(item);
}
}

事實上 MyMap() 不只適用於 int,而且可適用於任何型別,因此重構成 <T, R>

Refactor to Extension Method


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp
{
static class Program
{
static void Main(string[] args)
{

Enumerable
.Range(1, 3)
.MyMap(Double)
.ToList()
.ForEach(Console.WriteLine);

int Double(int x) => x * 2;
}

private static IEnumerable<R> MyMap<T, R>(this IEnumerable<T> data, Func<T, R> func)
{
foreach (var item in data)
{
yield return func(item);
}
}
}
}

20 行

1
2
3
4
5
6
7
private static IEnumerable<R> MyMap<T, R>(this IEnumerable<T> data, Func<T, R> func)
{
foreach (var item in data)
{
yield return func(item);
}
}

MyMap() 需要兩個參數,使用上不是那麼方便,而且也無法 Pipeline 般使用,因此將第一個參數加上 this,成為 Extension Method。

11 行

1
2
3
4
5
Enumerable
.Range(1, 3)
.MyMap(Double)
.ToList()
.ForEach(Console.WriteLine);

如此 MyMap() 就與 Range() 串起來了,而且也減少了一個參數。

Refactor to LINQ


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

namespace ConsoleApp
{
static class Program
{
static void Main(string[] args)
{

Enumerable
.Range(1, 3)
.Select(Double)
.ToList()
.ForEach(Console.WriteLine);

int Double(int x) => x * 2;
}
}
}

事實上 LINQ 早已提供 Select(),不必我們自己實作,其功能完全等效於自己實作的 MyMap()

一般 FP 世界,將這種 operator 稱為 Map,如 ECMAScript 有Array.prototype.map(),F# 有 List.map(),在 LINQ 則稱為 Select()

Refactor to Using Static


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

namespace ConsoleApp
{
static class Program
{
static void Main(string[] args)
{

Range(1, 3)
.Select(Double)
.ToList()
.ForEach(WriteLine);

int Double(int x) => x * 2;
}
}
}

使用 using static 之後,則 Range()WriteLine() 可進一步縮短,更符合 FP 風格。

Conclusion


  • 就算自己重構,也會重構出 Select() Higher Order Function,只是因為太常使用,LINQ 已經內建 Select()
  • Yield return 可實現 Lazy Evaluation,繼可節省記憶體,又可增進執行效率
  • 善用 using static,可讓 class 的 static method 更像 function

Sample Code


完整的範例可以在我的 GitHub 上找到

2018-10-01