- A+
原文 https://fsharpforfunandprofit.com/posts/recipe-part2/
这是关于 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 validate = Result.bind validateName >> Result.bind validateEmail let test() = let result1 = validate (Ok {Name="abc"; Email="a@c"}) printfn "%A" result1 let result2 = validate (Ok {Name="abc"; Email=""}) printfn "%A" result2 test()
安装了 .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
说到这里,一切迷雾已经解开,请回头再看本文开头那段代码,相信你现在已经可以轻松理解它了。