zoukankan      html  css  js  c++  java
  • 函数式编程之-重新认识泛型(2)

    回顾上一节,为了丰富建模类型,编程语言引入了泛型,例如Optional<T>,Result<T>等。我们把泛型也叫做类型提升(lifting),这样带来的问题是以往的函数不能再适应提升类型,试想之前已经存在一个a->b的函数,但是此时你拥有一个E<a>变量,你无法直接把E<a>传入到a->b的函数中。上一节还提到,一旦你的类型被提升(lifting),你应该竟可能的让他保持在提升的状态,而不是随意在提升类型(E<a>)和a直接来回切换。

    为了达到这个目的,数学家们发现了一些规律,通过一些函数来达到这个目的。例如:当你已经有一个定义好的函数a->b,而这时候又有一个被提升的类型E<a>,此时你可以通过map/select函数直接将a->b应用在E<a>上得到E<b>。

    从某种意义上来说,map/select函数有提升函数的作用。之所以a->b可以作用在E<a>上面,是因为map/select函数把函数a->b提升为E<a->b>。正因为如此,某些语言也将map函数叫做lift函数。

    return函数

    在继续往下介绍之前,我们先了解另一个函数return,有的语言也称为pure/unit/point

    return函数的作用在于将普通类型a提升为E<a>。例如下面的C#代码:

    var x = Optional.Some(10); //将int提升为Optional<int>
    var y = Optional.None<int>; //将int提升为Optional<int>
    var z = new List<int>(){1,2,3}; //将int提升为List<int>
    

    一般来说你并不需要单独定义return函数,但是当我们提到return函数的时候你应该要知道他的意图。

    通过map函数来提升函数

    除了return函数能够提升类型,map函数也有提升类型的作用。
    考虑下面的情况:

    let add1 x = x + 1
    let result = Some 2 |> Option.map add1
    

    给定一个函数add1: a->b,然后通过Option.map将add1提升为E<(a->b)>,并传入Some 2,得到结果Some 3。
    上面的函数add1只有一个参数,如果对拥有两个参数的函数做map会发生什么?

    let add x y = x + y
    let result' = Some 2 |> Option.map add
    

    因为add接受两个参数x和y,通过map提升并传入第一个参数Some 2,得到的结果result'是一个提升函数Option<(a->b)>。C#并不支持这种方式,C#中的Select方法只接受Func<TSource, TResult>,也就是说C#中的Select方法只接受一个参数的函数。你不能通过Select提升具有多个参数的函数。
    同理,通过map提升拥有3个参数的函数:

    let add x y z = x + y + z
    let result' = Some 2 |> Option.map add
    

    得到的result'是Option<(a->b->c)>。

    apply函数

    对于普通类型的函数a->b->c,你可以通过partial应用依次传入a和b,最终得到c。

    //定义一个函数 add: a -> b -> c
    let add a b =
        a + b
        
    let add10 = add 10  // add10: b -> c
    let result = add10 2 // result: 12
    

    我们已经知道有两种途径可以提升函数:return和map,那么:
    假如你有一个Option<(a->b->c)>的函数,你能否在提升类型的世界里做partial应用呢?

    // 通过return 函数创建一个提升函数Option<(a->b->c)>
    let add' = Some (fun x y -> x + y) 
    // 试图在add'函数上传入Some 2做partial application
    let add2' = add' (Some 2)
    

    上面的函数会发生编译失败,也就是说,对于一个提升函数,无法做partial application. 如果我们能够定义一个函数,他可以接受一个提升类型的函数和一个提升类型的参数,同时得到另一个提升类型的结果,那么我们的目的就达到了:

    下面是Option<T>类型的apply定义:

    module Option =
        let apply fOpt xOpt = 
            match fOpt,xOpt with
            | Some f, Some x -> Some (f x)
            | _ -> None
    

    有了apply函数就可以对提升类型的函数做partial应用了:

    let add' = Some (fun x y -> x + y) 
    let add2' = Some 2 |> Option.apply add'
    let add23' = Some 3 |> Option.apply add'2
    

    中缀表达式

    F#或者C#中的函数都是前缀表达式,例如有一个add函数,接受两个参数:

    let add x y = x + y
    let result = add x y // 函数名在前面,两个参数在后面
    

    但是数学中的运算符通常都是中缀表达式,例如数学中的加号运算符:

    let result = 1 + 2
    

    加号写在中间,而两个参数分别写在两边。同样的道理,任意一个拥有两个参数的函数,我们都可以通过定义运算符的方式,让他变为中缀表达式,例如在F#通过下面的方式定义运算符:

    let (<*>) = Option.apply
    

    有了运算符<*>,上面的apply过程就可以写成下面的样子:

    (Some add) <*> (Some 2) <*> (Some 3)
    

    上面的代码通过return函数来提升函数,我们知道map函数也可以提升函数:

    let (<!>) = Option.map
    let (<*>) = Option.apply
    
    add <!> (Some 2) <*> (Some 3)
    

    跟Functor Laws一样,同样有四个所谓的"Applicative Laws",这四个Laws我将不一一描述,从代码的角度来说,本文描述的apply函数就是所谓的Applicative Functor。

    map2和map3函数

    对于上面的实例,能够将拥有两个参数的函数提升,并且接受两个提升类型的过程,F#定义了一个函数叫做map2:

    let result = Option.map2 add (Some 2) (Some 3)
    

    同样的道理,如果是3个参数的函数,则可以通过map3函数来完成。

    什么样的类型支持map/apply/return?

    几乎所有你能用到的泛型都可以支持这三个函数,如果是你自己编写的泛型类型,请尝试添加这三个函数。

    applicative和apply到底有什么用?

    如果你看到这里你已经明白了什么是applicative,但是到底什么样的场景能够使用applicative呢?对于实际的软件工程到底有什么用呢?后来的文章将描述具体的用法,请持续关注。

  • 相关阅读:
    [转载]注解
    Spring可扩展的XML Schema机制 NamespaceHandlerSupport
    jvm中的年轻代 老年代 持久代 gc ----------转载
    反射原理
    舍入误差
    mysql突然宕机后事务如何处理?
    redis为什么设计成单线程并且还这么快?
    mysql架构学习
    用户级线程和内核级线程的区别
    G1垃圾收集器
  • 原文地址:https://www.cnblogs.com/xiandnc/p/9502472.html
Copyright © 2011-2022 走看看