• More on wrapper types


    原文地址:http://fsharpforfunandprofit.com/posts/computation-expressions-wrapper-types-part2/

    上一篇中,我们说明了包装类型的概念以及与computation expression的关系。在这一篇中,我们将介绍什么类型是合适的包装类型。

    什么样的类型可以是包装类型?

    每个computation expression必须要有相应的包装类型,那么什么样的类型可以作为包装类型呢?对包装类型是否有特殊的限制?

    有一个通用的原则为:

    • 任何带有泛型参数的类型均可以用作包装类型

    例如,你可以使用Option<T>, DbResult<T>等作为包装类型。也可以使用限制了类型参数的包装类型,如Vector<int>

    但是对于其他泛型类型如List<T>或者IEnumerable<T>如何呢?事实上,它们也可以被用作包装类型,我们一会就可以看到。

    非泛型包装类型是否可行?

    是否可以使用一个不带泛型参数的包装类型?

    例如,在以前的例子中我们见过一个string上的加法,如"1" + "2"。我们不能聪明地将string看成一个int的包装类型吗?这很酷,是吧

    我们来试一试,可是借助Bind和Return的签名帮助我们来实现。

    • Bind函数输入为一个元组。元组的第一部分是一个包装类型(这个例子中是string),第二部分是一个函数,这个函数以一个非包装类型作为输入,并将输入转变为一个包装类型(T -> M<U>)。这个例子中,函数签名是int -> string。
    • Return以一个非包装类型作为输入(这个例子中为int)并将输入转变为一个包装类型,这个例子中,Return签名为int -> string。

    以上函数签名如何指导实现过程?

    “包装”函数的实现,int -> string,是很简单的,就是int类型的“toString”方法。

    Bind函数必须去包装一个string为一个int,然后将这个int传入continuation函数f ,我们可以使用int.Parse函数实现这个去包装操作。

    如果Bind函数无法对一个string去包装,因为这个string不是一个有效数字,此时如何处理?这种情况下,绑定函数必须仍然返回一个包装类型(这里是string),所以我们可以只返回一个string如“error”。

    builder类的实现如下

    type StringIntBuilder() =
    
        member this.Bind(m, f) = 
            let b,i = System.Int32.TryParse(m)
            match b,i with
            | false,_ -> "error"
            | true,i -> f i
    
        member this.Return(x) = 
            sprintf "%i" x
    
    let stringint = new StringIntBuilder()

    现在我们可以尝试使用

    let good = 
        stringint {
            let! i = "42"
            let! j = "43"
            return i+j
            }
    printfn "good=%s" good

    如果有一个string无效,那么会发生什么

    let bad = 
        stringint {
            let! i = "42"
            let! j = "xxx"
            return i+j
            }
    printfn "bad=%s" bad

    看起来不错——在我们的工作流中,将strings看成ints。

    但是等下,有问题。

    我们给这个工作流一个输入,对输入进行去包装(使用let!),然后立即复包装它(使用return),这其中没有做其他任何事情。会发生什么情况?

    let g1 = "99"
    let g2 = stringint {
                let! i = g1
                return i
                }
    printfn "g1=%s g2=%s" g1 g2

    以上这段代码没有问题。输入g1和输出g2是相同的值,如我们所期望一样。

    但是如果是字符串转换为int时发生错误的情况呢?

    let b1 = "xxx"
    let b2 = stringint {
                let! i = b1
                return i
                }
    printfn "b1=%s b2=%s" b1 b2

    这种情况下,我们得到一个跟期望不同的行为。输入b1和输出b2不是相同的值。我们引入了不一致问题。

    这在实际中会是一个问题吗?我不清楚,但是我将避免它,使用一个不同的方法,如options,在所有情况都是一致的。

    工作流使用包装类型的原则

    有个问题,如下代码所示,这两段代码有什么不同,它们的行为是否不同?

    // fragment before refactoring
    myworkflow {
        let wrapped = // some wrapped value
        let! unwrapped = wrapped
        return unwrapped 
        } 
        
    // refactored fragment 
    myworkflow {
        let wrapped = // some wrapped value
        return! wrapped
        }

    答案是否定的,即它们的行为不应该不同。唯一的不同是在第二个例子中,unwrapped值已经被重构了,直接返回wrapped值。

    但是正如我们在前一小节中所见,如果不小心则会引入不一致问题。故,任何一种实现都应该遵循一些标准原则,总结如下:

    原则1:如果以一个非包装类型值开始,然后包装它(使用return),然后去包装它(使用bind),那么总是可以回到初始的非包装类型值

    这个原则以及下一个原则的关注点为:当包装和去包装值的时候不会丢失信息。

    用代码表示这个原则如下

    myworkflow {
        let originalUnwrapped = something
        
        // wrap it
        let wrapped = myworkflow { return originalUnwrapped }
    
        // unwrap it
        let! newUnwrapped = wrapped
    
        // assert they are the same
        assertEqual newUnwrapped originalUnwrapped 
        }

    原则2: 如果以一个包装类型值开始,然后去包装这个值(使用bind),然后包装它(使用return),则总是可以回到初始的包装类型值。

    这个原则跟上面的stringInt工作流一致。

    用代码表示则如下

    myworkflow {
        let originalWrapped = something
    
        let newWrapped = myworkflow { 
    
            // unwrap it
            let! unwrapped = originalWrapped
            
            // wrap it
            return unwrapped
            }
            
        // assert they are the same
        assertEqual newWrapped originalWrapped
        }

     原则3:如果创建一个子工作流,那它必须产生与主工作流相同的结果,就好像是将逻辑嵌入到主工作流中。

    这个原则要求正确组合。

    用代码演示则如下

    // inlined
    let result1 = myworkflow { 
        let! x = originalWrapped
        let! y = f x  // some function on x
        return! g y   // some function on y
        }
    
    // using a child workflow ("extraction" refactoring)
    let result2 = myworkflow { 
        let! y = myworkflow { 
            let! x = originalWrapped
            return! f x // some function on x
            }
        return! g y     // some function on y
        }
    
    // rule
    assertEqual result1 result2

    将“列表”作为包装类型

    之前提过List<T>或者IEnumerable<T>可以作为包装类型,但是怎么实现呢?在包装类型和非包装类型之间没有一对一的对应关系。

    这正是“包装类型”类比有一点点误导的地方。我们回想一下bind,bind是一种将一个表达式的输出与另一个表达式的输入联系起来的方法。

    我们已经看到,bind函数去包装一个类型,然后将continuation函数f 应用到这个去包装后的值上。但是没有任何规定说只能有一个未包装值。没有理由说我们不能依次应用continuation函数到一个list的每一项上。

    也就是说,我们能够写一个bind,这个bind的输入参数为由一个列表以及一个continuation函数f 组成的元组,且continuation函数f 每次处理这个列表中的一个元素,如下

    bind( [1;2;3], fun elem -> // expression using a single element )

    有了这个概念后,我们可以将一些bind链接起来如下

    let add = 
        bind( [1;2;3], fun elem1 -> 
        bind( [10;11;12], fun elem2 -> 
            elem1 + elem2
        ))

    但是我们忽略了一些重要的东西。传入bind的continuation函数f 必须要符合某种函数签名,即有一个未包装类型作为输入参数,并产生一个包装类型的输出。

    换句话说,continuation函数f 产生的结果必须总是一个新列表(因为类型包装M必须相同,而这里用列表来包装类型)

    bind( [1;2;3], fun elem -> // expression using a single element, returning a list )

    这样,我们则必须将上面那个链接起来的代码写成如下形式,其中elem1+elem2的结果被放入一个列表中

    let add = 
        bind( [1;2;3], fun elem1 -> 
        bind( [10;11;12], fun elem2 -> 
            [elem1 + elem2] // a list!
        ))

    所以我们bind方法的逻辑类似这样

    let bind(list,f) =
        // 1) for each element in list, apply f
        // 2) f will return a list (as required by its signature)
        // 3) the result is a list of lists

    现在又已经导致另一个问题了。因为continuation函数f 必须返回一个列表类型,而对作为输入参数的列表的每个元素应用函数f,则产生一个“列表的列表”,“列表的列表”不好,我们需要将它们转成简单的一阶列表。

    不过这已经很简单了,因为已经有一个模块函数能做到,即concat

    故将以上相关代码放到一起,我们有

    let bind(list,f) =
        list 
        |> List.map f 
        |> List.concat
    
    let added = 
        bind( [1;2;3], fun elem1 -> 
        bind( [10;11;12], fun elem2 -> 
    //       elem1 + elem2    // error. 
            [elem1 + elem2]   // correctly returns a list.
        ))

    现在我们知道了bind工作机制,就能够自己创建一个“列表工作流”

    • Bind对传入的列表的每一个元素应用continuation函数f,然后将“列表的列表”展平,得到一个一阶列表。List.collect就是一个能做到如此的库函数。
    • Return将未包装类型转为包装类型。这意味着将返回值包装成列表。
    type ListWorkflowBuilder() =
    
        member this.Bind(list, f) = 
            list |> List.collect f 
        
        member this.Return(x) = 
            [x]
    
    let listWorkflow = new ListWorkflowBuilder()
    let added = 
        listWorkflow {
            let! i = [1;2;3]
            let! j = [10;11;12]
            return i+j
            }
    printfn "added=%A" added
    
    let multiplied = 
        listWorkflow {
            let! i = [1;2;3]
            let! j = [10;11;12]
            return i*j
            }
    printfn "multiplied=%A" multiplied

    结果显示第一个集合中的每个元素,其中第一个集合由第二个集合中的每个元素组成。

    val added : int list = [11; 12; 13; 12; 13; 14; 13; 14; 15]
    val multiplied : int list = [10; 11; 12; 20; 22; 24; 30; 33; 36]

    非常奇妙,我们完全隐藏了列表枚举的逻辑,只暴露了工作流本身。

    “for”语法糖

    如果将列表和序列特别对待,我们可以增加一个语法糖:用一个更自然的东西代替let!

    用for..in..do表达式代替let!

    // let version
    let! i = [1;2;3] in [some expression]
    
    // for..in..do version
    for i in [1;2;3] do [some expression]

    为了让F#编译器能做到这点,我们需要增加一个For方法到我们到build类。For方法与一般的Bind方法的实现相同,但是要求接收一个序列类型(Bind函数对包装类型则没有限制为序列类型)

    type ListWorkflowBuilder() =
    
        member this.Bind(list, f) = 
            list |> List.collect f 
        
        member this.Return(x) = 
            [x]
    
        member this.For(list, f) = 
            this.Bind(list, f)
    
    let listWorkflow = new ListWorkflowBuilder()

    以下是使用方法

    let multiplied = 
        listWorkflow {
            for i in [1;2;3] do
            for j in [10;11;12] do
            return i*j
            }
    printfn "multiplied=%A" multiplied

    LINQ和“list工作流”

    这个 for element in collection do 看起熟悉吗?它非常接近于LINQ的from element in collection...语法。事实上,LINQ使用基本相同的方法在后台实现将一个查询表达式如from element in collection... 转为实际的调用方法。

    F#中,bind使用 形如 List.collect函数。LINQ中与List.collect等价的是 SelectMany扩展方法。如果知道SelectMany的工作原理,就可以实现相同的查询。参见Jon Skeet的博客 a helpful blog post 

    “包装类型”本质

    本篇我们已经见过很多包装类型了,并且已经说明每个computation expression必须有相对应的包装类型。但是,还记得一开始的那个logging例子吗?那个例子中没有包装类型,有let!在后台执行的逻辑,但是输入类型与输出类型相同,类型没有被改变。

    简单来说,可以将任意类型看作是自身的包装类型,但是,也可以从一个 更深的层次理解这一点。

    让我们回过头去考虑一下包装类型如List<T>到底是什么。

    如果有一个类型如List<T>,实际上这个类型不是一个真正的类型。List<int>是真正的类型,List<string>也是真正的类型,但是List<T>本身是不完整的,它缺少一个能变成真正类型的参数。

    一种方法是将List<T>看成一个函数,而不是一个类型,它是类型的抽象世界的一个函数,而不是值的具体世界的一个函数,但是正如那些将一个值映射到另一个值的函数一样,List<T>,其输入为类型(如int或者string),输出为其他类型(如List<int>或List<string>)。List<T>跟其他函数一样,它有一个参数,即“类型参数”,.net开发者所谓的泛型在计算机科学就是“参数多态”。

    一旦我们掌握了函数的概念,即,从一个类型产生另一个类型(称为”类型构造器“),就可以明白当说一个包装类型时,我们指的是一个类型构造器。

    但是,如果包装类型仅仅是一个函数,它将一个类型映射到另一个类型,那么,可以确定将一个类型映射到同样的类型的函数也符合吗?嗯,没错。“identity”函数符合我们的定义,可以被用作computation expression的包装类型。

    回到代码中来,我们可以定义一个“identity”工作流,它非常简单

    type IdentityBuilder() =
        member this.Bind(m, f) = f m
        member this.Return(x) = x
        member this.ReturnFrom(x) = x
    
    let identity = new IdentityBuilder()
    
    let result = identity {
        let! x = 1
        let! y = 2
        return x + y
        } 

    有了这些概念,我们可以知道先前讨论的logging的例子就是一个添加了打印log信息的“identity”工作流。

    总结

    本篇涵盖了很多主题,希望能对包装类型有个更清楚的认识。我们了解到如何在实际中使用包装类型。

    总结一下本篇中的几个关键点:

    • computation expression的主要作用是去包装一个类型以及复包装。
    • 可以很容易组合computation expression,因为Return的输出匹配Bind的输入,都是包装类型
    • 每个computation expression必须有一个相关的包装类型
    • 任何带有一个泛型参数的类型都可以被用作包装类型,即使是列表也是如此。
    • 当创建工作流时,需要确保工作流的实现满足三个有关包装、去包装以及组合的原则。
  • 相关阅读:
    私有云是伪命题:真正的私有云 ≈ 公有云
    云计算的重新构建架构:优化迁移策略
    五个顶级的大数据架构
    Algorithm Gossip: 费式数列
    Algorithm Gossip: 河内塔
    Mysql连接报错:Unknown system variable 'language'
    ssm整合的时候出现的abstactMethodArror 解决
    java集合整理
    Oracle的序列和索引
    关于java堆栈的理解与说明
  • 原文地址:https://www.cnblogs.com/sjjsxl/p/4989248.html
Copyright © 2020-2023  润新知