再把map方法貼過來一次
public static Option Map<TSource, TResult>(this Option source, Func<TSource, TResult> operate)
{
return source switch
{
None => source,
Some<TSource> { Data: var sourceData } =>
Option.Some(operate(sourceData)),
_ => throw new NotSupportedException()
};
}
我用擴充方法的方式去寫map,除了Option物件本身自己以外,方法簽章接受一個委派方法operate。注意一下operate的宣告Func<TSource, TResult>
,它接受一個與Some內部Data相同的型別,並且返回另一指定的型別,所以我可以這樣寫:
var option = Option.Some(3);
var result = option.Map(x=>x.ToString());
// result的型別是Some<string>
其中x⇒ x.ToString()
是一個Func<int,string>的lambda function,在這邊其實有一個專有名詞函子Functor,Functor是範疇(category)內的映射關係,這是什麼天書???沒關係,可以回來看上面的範例,我們可以將Some想像成一種容器,裡面裝了int的資料3,而Map方法最後輸出的結果是Some,透過函子我們將容器內int轉成了sting,可以參考這張圖
圖片來源:https://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html
回到Map方法,這個寫法是不是很眼熟?是的沒錯,就是C#使用者一天到晚使用的Linq!看看Match的方法簽章,我們知道它依賴於某個函數,這種函數在函數式設計裡稱為高階函數(Higher Order Function),所謂的高階函數滿足以下其中之一:
Linq語法就是將”數據的操作”這個行為抽象化,讓使用者將實際要做的操作定義成另一個函數輸入,這邊先把兩個Linq中常用的語法Select
與SelectMany
拿出來討論,這兩個方法都是對IEnumerable的擴充方法,而IEnumerable本身就是一個對T的容器
var numberList = new List<int>{1, 2, 3, 4, 5, 6};
var tripleList = numberList.Select(Triple);
// tripleList = [3,6,9,12,15,18]
int Triple(int source)
{
return source * 3;
}
numberList.ForEach(Console.WriteLine);
// 1,2,3,4,5,6
tripleList.ForEach(Console.WriteLine);
// 3,6,9,12,15,18
Select會將Source中的元素映射成新的集合,每個元素仍然在IEnumerable這個容器中,這邊可以注意一下經過映射後,原來的numberList並沒有改變,這種特性稱為不變性,也是無副作用,是FP中的重要觀念。那麼假設今天集合中又有集合的其況呢?
var rawList = new List<List<int>>
{
new List<int> { 1, 2 },
new List<int> { 3, 4 }
};
var selectList = rawList.Select(x => x);
// [[1,2],[3,4]]
var selectManyList = rawList.SelectMany(x => x);
// [1,2,3,4]
可以觀察到select提出的物件會被放到另一個IEnumerable容器,如果希望能將每個一個元素提取到相同的層級,就需要使用到SelectMany,這兩個方法分別對應到FP的兩個概念-map跟bind,我為了Option寫的Map方法輸入一個Option後也會返回一個Option,並不會將容器中的資料提取出來,這種不會將內部元素提取出容器就是Map,而與之對應的Bind就是能夠將元素從容器中提取出來,假以上面的Option範例,如果我今天希望輸入的數字大於5時會轉型失敗
var option = Option.Some<int>(6);
var mapResult = option.Map(ConvertToStringFailWhenHigherThanFive);
// mapResult的型別是Some<None>
var bindResult = option.Bind(ConvertToStringFailWhenHigherThanFive);
// BindResult的型別是None
Option ConvertToStringFailWhenHigherThanFive(int source) =>
source switch
{
> 5 => Option.None(),
_ => Option.Some(source.ToString())
};
// Bind實做
public static Option Bind<TSource>(this Option source, Func<TSource, Option> operate)
{
return source switch
{
None => source,
Some<TSource> { Data: var sourceData } => operate(sourceData),
_ => throw new NotSupportedException()
};
}
// 最後補一版C#語法糖
int? number = 6;
var nullableResult = number is {} value
? ConvertToStringOrNull(value)
: null;
int? ConvertToStringOrNull(int source) =>
source switch
{
> 5 => null,
_ => source.ToString()
};
很顯然的我們應該不會希望得到Some的結果,這時候就會需要Bind來提取資料,對應有一個專有名詞Monad。
今天初步介紹了接受函數輸入高階函數,並且提到了Functor跟Monad,以及Map跟Bind,Map跟Bind的不同在於對資料的抽象操作,Map操作完後會將資料留在原來的層次(Option、IEnumerable),而Bind則會向上提昇一個層級,明天會介紹輸出函數的HOF並且帶到柯里化。