函數語言程式設計之-bind函式
Bind函式
Bind函式在函數語言程式設計中是如此重要,以至於函數語言程式設計語言會為bind函式設計語法糖。另一個角度Bind函式非常難以理解,幾乎很少有人能通過簡單的描述說明白bind函式的由來及原理。 這篇文章試圖通過“人話”來描述bind函式,並通過淺顯的例項為零函數語言程式設計語言的開發者揭祕bind函式的作用及用法。
public string GetSomething(int id) { var x = GetFirstThing(id); if (x != null) { var y = GetSecondThing(x); if(y != null) { var z = GetThirdThing(y); if (z != null) { return z; } } } return null; }
你一定寫過類似的程式碼,估計你也明白這樣的程式碼看起來很醜陋,一層層的判空巢狀打亂了程式碼的主題結構。 有沒法讓他變的更優雅?當然你可以通過"early return"的做法,不過這種方式不在我們的討論範圍之內。 這種風格的程式碼存在一個明顯的code smell, GetFirstThing()/GetSecondThing()/GetThirdThing()等方法有可能返回null,我們說return null是一種不真確的做法,相關分析見拒絕空引用異常。使用Optional型別重構如下:
public Optional<string> GetSomething(int id) { var x = GetFirstThing(id); if (x.HasValue()) { var y = GetSecondThing(x); if(y.HasValue()) { var z = GetThirdThing(y); if (z.HasValue()) { return z; } } } return Optional.None<string>(); }
看起來程式碼結果跟之前一模一樣,重構後的程式碼並沒有變得更漂亮。不過現在的GetFirstThing()/GetSecondThing()/GetThirdThing()方法返回值為Optional<string>型別,不再是普通的string型別:
public Optional<string> GetFirstThing(int id)
{
//...
return Optional.None<string>();
}
重構後的這段程式碼很有意思,我們可以從函式組合的角度來讓整個程式碼段變的更加優雅。
組合
這段程式碼其實做了一件事,那就是通過呼叫三個函式GetFirstThing()/GetSecondThing()/GetThirdThing()來完成一個業務邏輯。從函數語言程式設計思想的角度出發,我們傾向於把若干個小的函式連線起來,根據以前學過的知識,只有這樣的兩個函式才能連線: 圖
GetFirstThing: int -> Optional<string>
GetSecondThing: string -> Optional<string>
GetThirdThing: string -> Optional<string>
顯然GetName和GetEmail是無法直接連線的,原因是GetFirstThing返回了Optional<string>型別,而GetSecondThing的輸入卻是一個普通的string型別。如果我們能夠在Optional<T>上擴充套件一個函式,函式接受一個簽名為T -> Optional<T>的函式,那麼我們就有可能將上面的三個函式串聯起來:
public static class Optional
{
public static Optional<T> Bind<T>(this Optional<T> input, Func<T, Optional<T>> f)
{
if (input.HasValue())
{
return f(input.Value);
}
return Optional.None<T>();
}
}
有了上面這個神奇的bind函式你就可以將上面的三個函式連線起來了:
public string GetSomething(int id)
{
return GetFirstThing(id).Bind(GetSecondThing).Bind(GetThirdThing);
}
用F#實現:
let address = getFirstThing id
|> bind getSecondThing
|> bind getThirdThing
通過bind函式我們成功將三個函式連線了起來, 同時將判空放在了bind函式裡,從而保持主要邏輯部分更加線性和清晰。
如何編寫屬於自己的bind函式
- 首先需要定義一個泛型型別E<a>,例如我們上面例子中提到的Optional<T>
- 編寫屬於Optional<T>的bind函式,bind函式的簽名為E<a> -> (f: a -> E<b>) -> E<b>。 接收一個E<a>,同時接受一個簽名為a -> E<b>的函式,返回E<b>。
List<T>中的bind函式
我們經常用的List<T>就是一個典型的泛型型別,那他上面有沒有bind函式?當然有,不過叫做SelectMany, Scala中也叫flatMap。 看一下SelectMany的方法簽名,正好符合bind函式的簽名要求:
public static IEnumerable<TResult> SelectMany<TSource,
TResult>(this IEnumerable<TSource> source, Func<TSource, IEnumerable<TResult>> selector)
{
//...
}
SelectMany可以用在什麼樣的場景中? 例如有這樣一個場景,一篇文章(paper)可以有若干章節(section)組成,每個章節(section)又有若干行(row)組成,每行(row)有若干單詞(word)組成。
問:給定一篇文章(paper),請找出大於10行(row)的章節(section),裡面排除註釋的行(row)總共的單詞(word)數量。
首先根據需求變下下面的若干函式:
private List<Paper.Section> GetSections(Paper paper)
{
return paper.Sections.Where(s => s.Rows.Count > 10).ToList();
}
private List<Paper.Section.Row> GetRows(Paper.Section section)
{
return section.Rows.Where(r=>!r.IsComment).ToList();
}
private List<Paper.Section.Row.Word> GetWords(Paper.Section.Row row)
{
return row.Words;
}
且看這三個函式的簽名:
GetSections: Papaer -> List<Section>
GetRows: Section -> List<Row>
GetWords: Row -> List<Word>
正好這就是就符合bind函式連線的需求:
var length = GetSections(paper)
.SelectMany(GetRows)
.SelectMany(GetWords)
.Count();
F#實現:
let words = getSections paper
|> bind getRows
|> bind getWords
words.Length
bind函式的語法糖支援
bind函式在函數語言程式設計中如此常見,以至於需要設計單獨的語法糖,Hask中叫do natation
, Scala中叫for comprehension
,F#用Computation expressions
:
list {
let! section = getSections(paper)
let! row = getRows(section)
let! word = getWord(row)
return word
}