F 函式式程式設計之 面向鐵道程式設計

2021-10-21 19:22:24 字數 3457 閱讀 1842

原文

參考 **不長,先看**吧,我在**後面寫講解。

type request = 

let validatename request =

match request with

| when name = "" ->

error "name must not be blank"

| _ -> ok request

let validateemail =

function

| when email = "" -> error "email must not be blank"

| request -> ok request

let test1() =

let validate =

result.bind validatename >> result.bind validateemail

let result1 = validate (ok )

printfn "%a" result1

let result2 = validate (ok )

printfn "%a" result2

test1()

安裝了 .net sdk 後,複製上面的**貼上到檔案中,儲存為 railway.fsx, 在控制台使用命令dotnet fsi railway.fsx即可執行。

這段**的目的是對 request 進行驗證,並優雅地處理錯誤。

為了保持簡單,我們只做了兩個簡單的驗證,但現實中可能需要對同乙個 request 進行很多個驗證,每一步都可能產生錯誤,因此必須想辦法優雅地處理錯誤。

在函式式程式設計中,如果函式 f1 的輸出恰好可以作為函式 f2 的輸入引數,那麼 f1 和 f2 就可以直接拼接起來變成 f3.

因此,只要我們想辦法讓每乙個驗證函式的輸入、輸出都相同,就能輕鬆地把它們拼接起來。

乙個可行的辦法就是採用標準庫里的 result.bind 函式 (

bind 函式是本文開頭那段**的關鍵,也是 railway oriented programming 的關鍵所在!

這個函式接受兩個引數: fn 和 result。

其中 result 的型別是 result, 它有兩種可能: ok 或 error。

當 result 是 ok 時,就用函式 fn 去處理它;當 result 是 error 時,則不會執行 fn。

最後,bind 函式也返回乙個 result。

簡單來說,bind 的作用是確保我們總能輸入乙個 result, 經過 fn 處理後,又總能輸出乙個 result。

在理解了 bind 函式的作用後,接下來的事情就非常容易理解了。

請看 validatename 和 validateemail, 其中 validateemail 用了乙個語法糖function, 其實它和 validatename 裡的match...with的作用是完全一樣的,我在這裡只是順便介紹一下這個語法糖而已。

這兩個函式雖然都輸出乙個 result, 但它們的輸入引數都是 request 而不是 result, 因此它們無法直接拼接起來。

此時,我們使用 bind, 看看會得到什麼:(注意看了,神奇的事情即將發生

let validate1 = result.bind validatename

let validate2 = result.bind validateemail

由於 bind 的型別是fn -> result -> result(其中 fn 是乙個函式,該函式的返回值也是乙個 result)

因此,當我們餵給它乙個 fn 函式時,它就會變成result -> result

也就是說,validate1 是result -> result, validate2 也是result -> result

也就是說,它們被 bind 了一下,就神奇地統一了輸入輸出,現在它們可以直接拼接了:

let validate = result.bind validatename >> result.bind validateemail
在原文裡有配圖,說明了這種程式設計模式與鐵道的相似之處,如有興趣請看原文。

在這裡我只說重點:這種鐵道有兩條軌道,一條是 ok, 一條是 error。

資料就像旅客,函式就像站點,資料在起點先走在 ok 軌道上,每到一站就進行一些處理(資料被函式處理,再傳遞至下乙個函式),如果發生錯誤,就切換到 error 軌道。

乙個重要的特性是:error 軌道就像快車道,一旦切換到 error 軌道,就再也不停站,直達終點。

說到這裡,一切迷霧已經解開,請回頭再看本文開頭那段**,相信你現在已經可以輕鬆理解它了。

我們還可以自定義乙個運算子>=>來進行拼接。

let (>=>) f1 f2 x =

match f1 x with

| ok y -> f2 y

| error z -> error z

let test2() =

let validate =

validatename >=> validateemail

let result1 = validate

printfn "%a" result1

|> validate

|> printfn "%a"

test2()

這裡,關注的重點是 validate 函式,它由兩個簽名相同的函式拼接而成,最終 validate 的簽名也與其中的每乙個函式相同。

比如在這個例子中,validatename 與 validateemail 的簽名都是 request -> result, 因此拼接後的 validate 也一樣是 request -> result.

前面我們介紹過 result.bind 函式,而 result.map 與它類似。

bind 的作用是將乙個函式 'a -> result 改造成 result -> result, 而 map 的作用則是將乙個普通的(與 result 完全不搭邊的)函式 'a -> 'b 改造成 result -> result。例如:

// request -> request

let canonicalizeemail input =

// result -> result

let validateandprocess =

result.bind validatename

>> result.bind validateemail

>> result.map canonicalizeemail

(注:本文為了表達上的簡潔和理解上的方便,有很多地方不夠嚴謹,更詳細更嚴謹的內容請看原文。)

F 程式設計 函式式程式設計之Records

當你想把資料組成乙個結構化的格式,而不需要太複雜的語法時,你可以使用f 中的record型別。record型別與c語言的struct型別基本一樣,儲存一組型別的值,通過欄位的值來獲取。定義乙個record型別很簡單,只需要在大括號內定義系列的名稱 型別就可以。要例項化乙個record,只需要提供對應...

函式式程式設計之 初窺F

大量講解函式式程式語言的書籍最終都會用fuctor,monad,monoids,範疇論等各種詞彙嚇退命令式語言玩家,所以我試圖避開這些問題,揭開這些複雜詞彙帶來的具有實戰意義的成果。另外我會盡量使用c 語言來描述函式式程式設計思想,因為c 某些語法和特性來自於函式式語言的啟發,但c 終究並不是正統的...

F 函式式程式設計之 隱藏運算

隱藏運算 是我發明的詞,它的正式名稱是 computation expressions 但 computation expressions 這個名稱實在讓人非常費解,也不能反映它的作用,不是乙個好名稱。它的作用是在背後對兩個表示式進行一些操作,讓表示式們表面上看起來簡單。請看例子 let divid...