iT邦幫忙

2022 iThome 鐵人賽

DAY 5
0

再把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://ithelp.ithome.com.tw/upload/images/20220916/201485946uNtGeu7ig.png

圖片來源:https://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html

什麼是高階函數

回到Map方法,這個寫法是不是很眼熟?是的沒錯,就是C#使用者一天到晚使用的Linq!看看Match的方法簽章,我們知道它依賴於某個函數,這種函數在函數式設計裡稱為高階函數(Higher Order Function),所謂的高階函數滿足以下其中之一:

  1. 接受函數作為輸入
  2. 將函數作為輸出

Linq語法就是將”數據的操作”這個行為抽象化,讓使用者將實際要做的操作定義成另一個函數輸入,這邊先把兩個Linq中常用的語法SelectSelectMany拿出來討論,這兩個方法都是對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的兩個概念-mapbind,我為了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並且帶到柯里化。


上一篇
Day4. Pattern Match
下一篇
Day6. Currying
系列文
Functional Programming with C#30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言