F# 函数式编程之 – 面向铁道编程

  • A+
所属分类:.NET技术
摘要

原文 https://fsharpforfunandprofit.com/posts/recipe-part2/这是关于 F# 的一个最受欢迎的网站里,最受欢迎的一篇文章《Railway oriented programming》。

原文 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 

说到这里,一切迷雾已经解开,请回头再看本文开头那段代码,相信你现在已经可以轻松理解它了。