原文地址:http://fsharpforfunandprofit.com/posts/computation-expressions-bind/

上一篇讨论了如何理解let作为一个能实现continuations功能的语法,并介绍了pipeInto函数能让我们增加钩子(处理逻辑)到continuation管道。

现在可以来一探究竟第一个builder方法——Bind,它是computation expression的核心。

介绍“Bind

MSDN的computation expression说明了let!表达式是Bind方法的语法糖。请看let!文档说明以及一个示例

// documentation
{| let! pattern = expr in cexpr |} // real example
let! x = in some expression

Bind方法文档说明及示例

// documentation
builder.Bind(expr, (fun pattern -> {| cexpr |})) // real example
builder.Bind(, (fun x -> some expression))

Bind有两个参数,一个表达式(43)和一个lambda

lambda的参数x绑定到Bind的第一个参数

Bind的参数顺序与let!的参数顺序相反

let!表达式链接起来

let! x =
let! y =
let! z = x + y

编译器会转换成调用Bind,就像这样

Bind(, fun x ->
Bind(, fun y ->
Bind(x + y, fun z ->
etc

我想你会发现有似曾相识的感觉。没错,pipeInto函数跟Bind函数完全相同。

实际上,computation expressions 仅是一种创建语法糖的方式,以让我们自己来实现想做的事情。

"bind" 函数

bind”是一个标准的函数模式,不依赖于computation expression

首先,为何称之为“bind”?嗯,正如我们所见,“bind”函数可被想象成将一个输入值传入到一个函数,即,绑定一个值到一个函数的参数上。可见“bind”类似于管道或组合。

事实上,可以将它转成一个中缀操作

let (>>=) m f = pipeInto(m,f)

顺便一提,“>>=”是将bind写成中缀操作符的标准方式。如果你曾在F#代码中见过这个符号,这个符号的意思就是这里绑定的含义。

再看之前“安全除法”的例子,现在可以将代码写成如下

let divideByWorkflow x y w z =
x |> divideBy y >>= divideBy w >>= divideBy z

这种方式与普通的管道或者组合的区别不是很明显,但是有两点值得一提

  • 首先,bind函数在每个场景下有额外的自定义行为(如前面文章中logging和安全除法的例子,分别有额外的打印log和对除数是否为0的判断),它不像管道或者组合,它不是一个泛型函数
  • 其次,输入参数类型(如上面的m)不一定与函数参数(如上面的f)的输出类型相同,因此这个bind所做的事情就是优雅的处理这个不匹配以让函数可以被链接起来

我们可以在下一篇看到,bind是与某种“包装(wrapper)类型”配合使用,它的值参数可能是WrapperType<TypeA>bind函数的函数参数的签名总是TypeA -> WrapperType<TypeB>

在“安全除法”的例子中,包装类型是Option。值参数(对应上面的m)的类型是Option<int>,函数参数(对应上面的f)的签名是int -> Option<int>

再看一个例子,其中使用了中缀绑定函数

let (>>=) m f =
printfn "expression is %A" m
f m let loggingWorkflow =
>>= (+) >>= (*) >>= id

这个例子中,没有包装类型。所有的都是int类型。但即使如此,bind也有幕后打印logging的这种行为。

Option.bind 和回顾"maybe" 工作流

F#库中,你可以在很多地方看到Bind函数。现在你知道它们是什么。

一个特别有用的例子是Option.bind,这跟我们上面写的代码的功能相同,即

如果输入参数为None,那不再调用continuation函数。

如果输入参数为Some,那调用continuation函数,并将Some的内容作为参数传入到函数中。

下面是我们自己写的函数

let pipeInto (m,f) =
match m with
| None ->
None
| Some x ->
x |> f

然后是Option.bind的实现

module Option =
let bind f m =
match m with
| None ->
None
| Some x ->
x |> f

Option.bind重写“maybe”工作流

type MaybeBuilder() =
member this.Bind(m, f) = Option.bind f m
member this.Return(x) = Some x

复习不同的实现方法

迄今已经用四种不同的方法实现“安全除法”。将它们放在一起,并作比较

首先是最原始的版本,它使用了一个显式的工作流

module DivideByExplicit = 

    let divideBy bottom top =
if bottom =
then None
else Some(top/bottom) let divideByWorkflow x y w z =
let a = x |> divideBy y
match a with
| None -> None // give up
| Some a' -> // keep going
let b = a' |> divideBy w
match b with
| None -> None // give up
| Some b' -> // keep going
let c = b' |> divideBy z
match c with
| None -> None // give up
| Some c' -> // keep going
//return
Some c'
// test
let good = divideByWorkflow
let bad = divideByWorkflow

其次,使用我们自己定义的函数的版本(也就是“pipeInto”)

module DivideByWithBindFunction = 

    let divideBy bottom top =
if bottom =
then None
else Some(top/bottom) let bind (m,f) =
Option.bind f m let return' x = Some x let divideByWorkflow x y w z =
bind (x |> divideBy y, fun a ->
bind (a |> divideBy w, fun b ->
bind (b |> divideBy z, fun c ->
return' c
))) // test
let good = divideByWorkflow
let bad = divideByWorkflow

然后,使用computation expression的版本

module DivideByWithCompExpr = 

    let divideBy bottom top =
if bottom =
then None
else Some(top/bottom) type MaybeBuilder() =
member this.Bind(m, f) = Option.bind f m
member this.Return(x) = Some x let maybe = new MaybeBuilder() let divideByWorkflow x y w z =
maybe
{
let! a = x |> divideBy y
let! b = a |> divideBy w
let! c = b |> divideBy z
return c
} // test
let good = divideByWorkflow
let bad = divideByWorkflow

最后,用中缀操作来实现绑定

module DivideByWithBindOperator = 

    let divideBy bottom top =
if bottom =
then None
else Some(top/bottom) let (>>=) m f = Option.bind f m let divideByWorkflow x y w z =
x |> divideBy y
>>= divideBy w
>>= divideBy z // test
let good = divideByWorkflow
let bad = divideByWorkflow

bind函数是非常强大的。下一篇我们将会看到结合bind和包装类型来创造一种优雅的方式,并用这种方式传递额外的信息。

练习:你能理解多少?

在进入下一篇之前,何不来测试下自己是否已经理解之前的内容?

第一部分——创建一个工作流

首先,创建一个函数,将字符串解析成int型:

let strToInt str = ???

然后,创建一个computation expression builder类,用在工作流,如下所示

let stringAddWorkflow x y z =
yourWorkflow
{
let! a = strToInt x
let! b = strToInt y
let! c = strToInt z
return a + b + c
} // test code
let good = stringAddWorkflow "" "" ""
let bad = stringAddWorkflow "" "xyz" ""

解析:

strToInt函数类似上面的divide函数,故可以写出函数定义如下

open System

let strToInt (str: string) =
try
let res = Convert.ToInt32 str
Some res
with
| _ -> None

这里也可以换一种转换为int类型的方法,如下

let strToInt str =
let b, i = Int32.TryParse str
match b, i with
| false, _ -> None
| true, _ -> Some i

表示工作流的builder类写成如下

type YourWorkflowBuilder() =

    member this.Bind(x, f) =
    match x with
    | None -> None
| Some a -> f a
member this.Return(x) =
Some x

最后实例化这个类

let yourWorkflow = new YourWorkflowBuilder()

运行测试代码,结果为

val good : int option = Some
val bad : int option = None

第二部分——创建一个bind函数

通过第一部分后,增加两个函数

let strAdd str i = ???
let (>>=) m f = ???

然后用上面的两个函数,可以将代码写成如下形式

let good = strToInt "" >>= strAdd "" >>= strAdd ""
let bad = strToInt "" >>= strAdd "xyz" >>= strAdd ""

解析:

首先很容易写出>>=运算符的定义

let (>>=) m f = Option.bind f m

然后,由第一部分可知,strToInt函数返回结果类型为int option,通过>>=运算符传给strAdd str函数(柯里化),而>>=运算符内部的bind函数会对这个int option去包装化为int类型,然后将这个int类型参数传给strAdd str函数(参见Option.bind函数的定义),故可知strAdd函数签名为

strAdd: str:string -> i: int -> int option

尝试写出strAdd的函数定义

let strAdd str i =
match strToInt str with
| None -> None
| Some a -> Some (a + i)

最后运行上面的测试代码,结果为

val good : int option = Some
val bad : int option = None

总结

Computation expressionscontinuation passing(后继传递)的语法糖,隐藏了链接逻辑。

bind是关键函数,用来连接前一步的输出到下一步的输入。

符号>>=是将bind写成中缀操作符的标准方式

Introducing 'bind'的更多相关文章

  1. MQ:Introducing Advanced Messaging

    原文地址:http://www.yourenterprisearchitect.com/2011/11/introducing-advanced-messaging.html. Introducing ...

  2. [中文翻译] ASP.NET 5 简介(Introducing ASP.NET 5,原作ScottGu 2015/2/23)

    本文出处  [中文翻译] ASP.NET 5 简介(Introducing ASP.NET 5,原作ScottGu 2015/2/23) 这是我的文章备份 http://www.dotblogs.co ...

  3. std::bind接口与实现

    前言 最近想起半年前鸽下来的Haskell,重温了一下忘得精光的语法,读了几个示例程序,挺带感的,于是函数式编程的草就种得更深了.又去Google了一下C++与FP,找到了一份近乎完美的讲义,然后被带 ...

  4. JS核心系列:浅谈 call apply 与 bind

    在JavaScript 中,call.apply 和 bind 是 Function 对象自带的三个方法,这三个方法的主要作用是改变函数中的 this 指向,从而可以达到`接花移木`的效果.本文将对这 ...

  5. UWP中新加的数据绑定方式x:Bind分析总结

    UWP中新加的数据绑定方式x:Bind分析总结 0x00 UWP中的x:Bind 由之前有过WPF开发经验,所以在学习UWP的时候直接省略了XAML.数据绑定等几个看着十分眼熟的主题.学习过程中倒是也 ...

  6. 【java】Naming.bind和Registry.bind区别

    Naming类和Registry类均在java.rmi包 Naming类通过解析URI绑定远程对象,将URI拆分成主机.端口和远程对象名称,使用的仍是Registry类. public static ...

  7. x:bind不支持样式文件 或 此Xaml文件必须又代码隐藏类才能使用{x:Bind} 解决办法

    这两天学习UWP开发,发现一个很有趣的问题,就是我题目中的描述的. 我习惯了在ResourceDictionary中写样式文件,但是发现用x:Bind时会有问题 如果是写在Style里,则提示 “x: ...

  8. Mach-O 的动态链接(Lazy Bind 机制)

    ➠更多技术干货请戳:听云博客 动态链接 要解决空间浪费和更新困难这两个问题最简单的方法就是把程序的模块相互分割开来,形成独立的文件,而不再将它们静态的链接在一起.简单地讲,就是不对那些组成程序的目标文 ...

  9. 【Win10】UAP/UWP/通用 开发之 x:Bind

    [Some information relates to pre-released product which may be substantially modified before it's co ...

随机推荐

  1. oracle登录时shared memory realm does not exist的解决方法

    解决办法:1.用CMD进入命令行2.sqlplus /nolog 3.conn / as sysdba4.startup   然后用sqlplus进入命令  

  2. Secondary Namenode - What it really do?

    原文链接:http://blog.madhukaraphatak.com/secondary-namenode---what-it-really-do/ Secondary Namenode is o ...

  3. gulp 安装步骤

    第一步:安装node 搭建node环境:进入官网 http://nodejs.org  ,然后点击的绿色的 install 按钮,下载完成后直接运行程序. 第二步:使用命令行 (1)输入指令:node ...

  4. vlc-android1.8.0的全部源代码[包括C语言]

    我们基于vlc,整理出了vlc-android1.8.0的全部源代码, 并增加了LibVLC的简单调用, 您只需要7行代码,就可以完成调用,和原生的MediaPlayer类似. 下载地址https:/ ...

  5. 常用的html标签大全

    html标签大全 一.文字 1.标题文字 <h#>..........</h#> #=1~6:h1为最大字,h6为最小字 2.字体变化 <font>........ ...

  6. Android 不能返回 parent Activity 的问题

    使用 ActionBar,开启返回按钮: 在 Activity 的 onCreate 中添加下面代码 getSupportActionBar().setDisplayHomeAsUpEnabled(t ...

  7. 递归——CPS(一)

    程序中为什么需要栈stack? 普通的程序中,接触到子程序和函数的概念,很直观地,调用子程序时,会首先停止当前做的事情,转而执行被调用的子程序,等子程序执行完成后,再捡起之前挂起的程序,这有可能会使用 ...

  8. 安卓activity之间值共享解决办法,tabhost之间共享父类值,字符串类型的转换,获取每一个listview的item

    1.tabhost父类值共享的解决办法 dianzhanliebiao.java是传值页面,zhuyemian.java放的是tabhost,dianzhangaikuang.java是tabhost ...

  9. [Q]升级/重新获取授权步骤

    若因需要升级或授权文件失效(重装系统或其他原因),在服务期内可通过下面的步骤操作. 注:168元版提供2年升级及售后支持,118元版的提供1升级及售后支持. 步骤如下: 1. 重新获取CAD批量打图精 ...

  10. <密码的实现>输入密码的时候,显示“*”,而不是显示输入内容

    一开始还以为用C语言和C++不能实现输入密码的时候显示出“*”而不显示输入的内容呢!没想到偶然的机会试出了用while循环结构可以实现.以下是我整理的C语言和C++的代码,供初学者参考. 这是C语言实 ...