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); } }
|
因此改用 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 上找到