原文 https://fsharpforfunandprofit.com/posts/recipe-part2/
參考 https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/results
這是關于 F# 的一個最受歡迎的網站里,最受歡迎的一篇文章《Railway oriented programming》,
代碼不長,先看代碼吧,我在代碼后面寫講解,
type Request = {Name:string; Email:string}
let validateName request =
match request with
| {Name=name; Email=_} when name = "" ->
Error "name must not be blank"
| _ -> Ok request
let validateEmail =
function
| {Name=_; Email=email} 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 {Name="abc"; Email="a@c"})
printfn "%A" result1
let result2 = validate (Ok {Name="abc"; Email=""})
printfn "%A" result2
test1()
安裝了 .NET SDK 后,復制上面的代碼粘貼到檔案中,保存為 railway.fsx, 在控制臺使用命令 dotnet fsi railway.fsx 即可運行,
這段代碼的目的是對 Request 進行驗證,并優雅地處理錯誤,
為了保持簡單,我們只做了兩個簡單的驗證,但現實中可能需要對同一個 Request 進行很多個驗證,每一步都可能產生錯誤,因此必須想辦法優雅地處理錯誤,
在函式式編程中,如果函式 f1 的輸出恰好可以作為函式 f2 的輸入引數,那么 f1 和 f2 就可以直接拼接起來變成 f3.
因此,只要我們想辦法讓每一個驗證函式的輸入、輸出都相同,就能輕松地把它們拼接起來,
一個可行的辦法就是采用標準庫里的 Result.bind 函式 (https://github.com/dotnet/fsharp/blob/main/src/fsharp/FSharp.Core/result.fs)
bind 函式是本文開頭那段代碼的關鍵,也是 Railway oriented programming 的關鍵所在!
關于 Result.bind 函式
這個函式接受兩個引數: fn 和 result,
其中 result 的型別是 Result, 它有兩種可能: Ok 或 Error,
當 result 是 Ok 時,就用函式 fn 去處理它;當 result 是 Error 時,則不會執行 fn,
最后,bind 函式也回傳一個 Result,
簡單來說,bind 的作用是確保我們總能輸入一個 Result, 經過 fn 處理后,又總能輸出一個 Result,
關于 validateName 和 validateEmail
在理解了 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 {Name="abc"; Email="a@c"}
printfn "%A" result1
{Name="abc"; Email=""}
|> validate
|> printfn "%A"
test2()
這里,關注的重點是 validate 函式,它由兩個簽名相同的函式拼接而成,最終 validate 的簽名也與其中的每一個函式相同,
比如在這個例子中,validateName 與 validateEmail 的簽名都是 Request -> Result, 因此拼接后的 validate 也一樣是 Request -> Result.
方便好用的 Result.map 函式
前面我們介紹過 Result.bind 函式,而 Result.map 與它類似,
bind 的作用是將一個函式 'a -> Result 改造成 Result -> Result, 而 map 的作用則是將一個普通的(與 Result 完全不搭邊的)函式 'a -> 'b 改造成 Result -> Result,例如:
// Request -> Request
let canonicalizeEmail input =
{ input with Email = input.Email.Trim().ToLower() }
// Result -> Result
let validateAndProcess =
Result.bind validateName
>> Result.bind validateEmail
>> Result.map canonicalizeEmail
(注:本文為了表達上的簡潔和理解上的方便,有很多地方不夠嚴謹,更詳細更嚴謹的內容請看原文,)
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/233319.html
標籤:.NET技术
上一篇:字串轉換注意編碼
