zoukankan      html  css  js  c++  java
  • Computation expressions: Introduction

    本文仅为对原文的翻译,主要是记录以方便以后随时查看。原文地址为http://fsharpforfunandprofit.com/posts/computation-expressions-intro/

    背景

    是时候揭开计算表达式(Computation expression)的神秘面纱了。现有的解释说明都令人难以理解。比如查阅MSDN官方说明,则对初学者来说虽然简单明确,却对理解没有什么太大帮助。例如当你看到如下代码

    {| let! pattern = expr in cexpr |}

    它只是如下方法调用的一个简单的语法糖:

    builder.Bind(expr, (fun pattern -> {| cexpr |}))

    但,这是个什么鬼?

    实战

    首先看一段简单的代码,然后再用Computation expression实现同样的功能。

    let log p = printfn "expression is %A" p
    
    let loggedWorkflow = 
        let x = 42
        log x
        let y = 43
        log y
        let z = x + y
        log z
        //return
        z

    运行结果为

    expression is 42
    expression is 43
    expression is 85

    看起来很简单,但是每次都显式的调用log语句还是挺烦的,考虑隐藏这些log语句的方法,这时候,Computation expression派上用场。

    首先定义一个类型

    type LoggingBuilder() =
        let log p = printfn "expression is %A" p
    
        member this.Bind(x, f) = 
            log x
            f x
    
        member this.Return(x) = 
            x

    先不用管这段代码中的BindReturn成员方法,后面会给出解释。接着看如下代码,实现刚才的log功能

    let logger = new LoggingBuilder()
    
    let loggedWorkflow = 
        logger
            {
            let! x = 42
            let! y = 43
            let! z = x + y
            return z
            }

    运行这段代码可以获得跟刚才同样的输出结果,但是很明显,刚才代码中重复的log语句已经被隐藏了。

    安全除法

    现在让我们看一个经典的例子。

    假如要除以一系列的数,即一个接一个将这些数作为除数,但是这些数其中可能有0。如何处理?抛出一个异常会使代码丑陋,使用option类型好像是一个不错的方法。

    先定义一个帮助函数,实现除法功能并返回一个int option,正常情况下则为Some,否则为None。然后将这些除法过程链接起来,并且在每个除法过程后判定除法是否成功(返回Some),只有在成功的时候才会继续下一个除法过程。帮助函数如下

    let divideBy bottom top =
        if bottom = 0
        then None
        else Some(top/bottom)

    注意第一个参数为除数,故我们可以将除法表达式写成 12 |> divideBy 3 (表示12/3)的形式,这样更容易将整个除法过程串联起来。

    看一个具体的实例,用三个数依次去除一个初始数

    let divideByWorkflow init x y z = 
        let a = init |> divideBy x
        match a with
        | None -> None  // give up
        | Some a' ->    // keep going
            let b = a' |> divideBy y
            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'

    函数调用

    let good = divideByWorkflow 12 3 2 1
    let bad = divideByWorkflow 12 3 0 1

    bad变量为None,因为有一个除数为0

    注意到以上例子中,返回结果必须为int option,不能返回int。然后,这个例子中的连续测试除法是否失败的代码依然丑陋,考虑使用computation expression

    我们定义一个新的类型如下,并实例化

    type MaybeBuilder() =
    
        member this.Bind(x, f) = 
            match x with
            | None -> None
            | Some a -> f a
    
        member this.Return(x) = 
            Some x
       
    let maybe = new MaybeBuilder()

    重写除以一系列数的工作流的函数,隐藏了之前的判断分支逻辑

    let divideByWorkflow init x y z = 
        maybe 
            {
            let! a = init |> divideBy x
            let! b = a |> divideBy y
            let! c = b |> divideBy z
            return c
            } 

    测试以上函数,可得到同样的结果

    let good = divideByWorkflow 12 3 2 1 

    let bad = divideByWorkflow 12 3 0 1

    链接“or else” 测试

    上一个例子中,我们在某一步除法执行成功后才继续执行下一个除法,但是,有时候的控制流程并非如此,而是“or else”,即,第一个事情没有成功,则尝试第二个事情,第二个事情如果也失败,则尝试第三个事情,依次类推。

    看一个简单例子。假设我们有三个字典,并且我们想查找某一键对应的值,查询每一个字典的结果可能是成功或者失败。

    let map1 = [ ("1","One"); ("2","Two") ] |> Map.ofList
    let map2 = [ ("A","Alice"); ("B","Bob") ] |> Map.ofList
    let map3 = [ ("CA","California"); ("NY","New York") ] |> Map.ofList
    
    let multiLookup key =
        match map1.TryFind key with
        | Some result1 -> Some result1   // success
        | None ->   // failure
            match map2.TryFind key with
            | Some result2 -> Some result2 // success
            | None ->   // failure
                match map3.TryFind key with
                | Some result3 -> Some result3  // success
                | None -> None // failure

    这个查询函数的使用如下

    multiLookup "A" |> printfn "Result for A is %A" 
    multiLookup "CA" |> printfn "Result for CA is %A" 
    multiLookup "X" |> printfn "Result for X is %A"

    代码运行良好,但同样查询函数multiLookup的定义代码太烦,简化一下,首先定义一个bulider类如下

    type OrElseBuilder() =
        member this.ReturnFrom(x) = x
        member this.Combine (a,b) = 
            match a with
            | Some _ -> a  // a succeeds -- use it
            | None -> b    // a fails -- use b instead
        member this.Delay(f) = f()
    
    let orElse = new OrElseBuilder()

    重写查询函数

    let map1 = [ ("1","One"); ("2","Two") ] |> Map.ofList
    let map2 = [ ("A","Alice"); ("B","Bob") ] |> Map.ofList
    let map3 = [ ("CA","California"); ("NY","New York") ] |> Map.ofList
    
    let multiLookup key = orElse {
        return! map1.TryFind key
        return! map2.TryFind key
        return! map3.TryFind key
        }

    使用示例代码如下,运行后发现结果同期望一样

    multiLookup "A" |> printfn "Result for A is %A" 
    multiLookup "CA" |> printfn "Result for CA is %A" 
    multiLookup "X" |> printfn "Result for X is %A"

    带回调的异步调用

    .net中异步操作的标准方法是使用AsyncCallback delegate,这在异步操作完成时被调用。

    举个例子,网页下载

    open System.Net
    let req1 = HttpWebRequest.Create("http://tryfsharp.org")
    let req2 = HttpWebRequest.Create("http://google.com")
    let req3 = HttpWebRequest.Create("http://bing.com")
    
    req1.BeginGetResponse((fun r1 ->     // 请求1异步获取响应,完成后,请求2异步获取响应,完成后,请求3异步获取响应
        use resp1 = req1.EndGetResponse(r1)
        printfn "Downloaded %O" resp1.ResponseUri
    
        req2.BeginGetResponse((fun r2 -> 
            use resp2 = req2.EndGetResponse(r2)
            printfn "Downloaded %O" resp2.ResponseUri
    
            req3.BeginGetResponse((fun r3 -> 
                use resp3 = req3.EndGetResponse(r3)
                printfn "Downloaded %O" resp3.ResponseUri
    
                ),null) |> ignore
            ),null) |> ignore
        ),null) |> ignore

    以上代码使用太多的BeginGetResponse和EndGetResponse,以及嵌套的lambda,使得代码阅读费力。

    事实上,在需要连接回调函数的代码中,管理这种级联方法总是显得困难,这甚至被称为 "Pyramid of Doom"(尽管 none of the solutions are very elegant, IMO)

    当然,在F#中我们将不再写类似的代码,因为F#有内建的async computation expression ,这简化了代码。

    open System.Net
    let req1 = HttpWebRequest.Create("http://tryfsharp.org")
    let req2 = HttpWebRequest.Create("http://google.com")
    let req3 = HttpWebRequest.Create("http://bing.com")
    
    async {
        use! resp1 = req1.AsyncGetResponse()  
        printfn "Downloaded %O" resp1.ResponseUri
    
        use! resp2 = req2.AsyncGetResponse()  
        printfn "Downloaded %O" resp2.ResponseUri
    
        use! resp3 = req3.AsyncGetResponse()  
        printfn "Downloaded %O" resp3.ResponseUri
    
        } |> Async.RunSynchronously

    在这个系列的后面部分会看到 async 工作流是如何实现的。

    总结

    至此我们见到一些简单的computation expression的例子。

    • logging例子中,我们想在每一步中添加一些自定义的逻辑,如打印log信息。
    • 安全除法例子中,我们想更为优雅的处理除法出错的情况,以便我们更加专注其他一些事情。
    • 在多字典查询例子中,我们想在第一次查询字典成功后就结束并返回。
    • 最后在异步操作例子中,我们想隐藏大段的回调函数的代码。

    这些例子的一个共同点就是在每个表达式中,computation expression做了一些后台的事情。

    打一个不是很好的比方,可以把computation expression想象成SVN或者Git的一个提交后钩子,或者数据库的每次更新后被调用的触发器。这就是computation expression,它可以隐藏一些代码,从而让我们更专注于业务逻辑。

    至于computation expressionworkflow(工作流)之间的区别,我使用computation expression表示{...}let!语法,而workflow(工作流)表示具体实现。当然,不是所有computation expression的实现都是工作流,例如,说“async”工作流或者“maybe”工作流是合适的,但是说“seq”工作流就显得不太合适。

    也就是说,如下面的代码

    maybe 
        {
        let! a = x |> divideBy y 
        let! b = a |> divideBy w
        let! c = b |> divideBy z
        return c
        } 

    可以说maybe是我们使用的工作流,而{ let! a = .... return c }computation expression

    附:

    state type的文章。

  • 相关阅读:
    numpy.argmax 用在求解混淆矩阵用
    1、VGG16 2、VGG19 3、ResNet50 4、Inception V3 5、Xception介绍——迁移学习
    Tensorflow深度学习之十二:基础图像处理之二
    tensorflow实现图像的翻转
    成都Uber优步司机奖励政策(1月13日)
    北京Uber优步司机奖励政策(1月13日)
    滴滴快车奖励政策,高峰奖励,翻倍奖励,按成交率,指派单数分级(1月13日)
    天津Uber优步司机奖励政策(1月11日~1月17日)
    苏州Uber优步司机奖励政策(1月11日~1月17日)
    南京Uber优步司机奖励政策(1月11日~1月17日)
  • 原文地址:https://www.cnblogs.com/sjjsxl/p/4978263.html
Copyright © 2011-2022 走看看