美文网首页程序员写作数学·编程·诗词·音乐·人Java Web 核心技术
函数式编程与面向对象编程[1]: Lambda表达式 函数柯里化

函数式编程与面向对象编程[1]: Lambda表达式 函数柯里化

作者: 光剑书架上的书 | 来源:发表于2016-05-03 01:52 被阅读714次

    函数式编程与面向对象编程[1]: Lambda表达式 函数柯里化 高阶函数.md

    之剑

    2016.5.2 11:19:09

    什么是lambda表达式

    例子

    For example, in Lisp the 'square' function can be expressed as a lambda expression as follows:

    (lambda (x) (* x x))
    

    定义

    “Lambda 表达式”(lambda expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包(注意和数学传统意义上的不同)。

    Lambda calculus (also written as λ-calculus) is a formal system in mathematical logic for expressing computation based on function abstraction and application using variable binding and substitution. It was first introduced by mathematician Alonzo Church in the 1930s as part of an investigation into the foundations of mathematics. Lambda calculus is a universal model of computation equivalent to a Turing machine (Church-Turing thesis, 1937). Its namesake, Greek letter lambda (λ), is used in lambda terms (also called lambda expressions) to denote binding a variable in a function.

    Lambda calculus may be typed and untyped. In typed lambda calculus functions can be applied only if they are capable of accepting the given input's "type" of data.

    Lambda calculus has applications in many different areas in mathematics, philosophy,linguistics, and computer science. Lambda calculus has played an important role in the development of the theory of programming languages. Functional programming languages implement the lambda calculus. Lambda calculus also is a current research topic in Category theory.

    λ演算(λ-calculus)

    Lambda演算和函数式语言的计算模型天生较为接近,Lambda表达式一般是这些语言必备的基本特性。如:

    Scheme:

        
    (lambda(x)(+x1))
    
    

    Haskell:

        
    \x->x+1
    
    

    λ演算是一套用于研究函数定义、函数应用和递归的形式系统。函数的作用 (application) 是左结合的:

    f x y = (f x) y
    

    它包括一条变换规则 (变量替换) 和一条函数定义方式.

    比如说,下面的这个圆锥曲线二元二次函数:

    $$f(x,y)=x2+y2$$

    让我们用其他的一些方式来表达:

    匿名函数表达

    $$(x,y) \rightarrow x^2 + y^2$$

    柯里化表达

    $$x \rightarrow (y \rightarrow x^2 + y^2 )$$

    演算过程

    为了更加简单的理解其演算过程, 分解各个步骤如下:

    数学函数表达式: \(f(5,2) = 5^2 + 2^2 = 29\)

    匿名函数式:\( ((x,y) \rightarrow (x^2 + y^2)) (5,2) = 5^2 + 2^2 = 29\)

    柯里化函数式: \( (x \rightarrow ( y \rightarrow (x^2 + y^2) )(5))(2)=(y \rightarrow (52+y2))(2) = 52+22=29 \)

    lambda演算的历史

    λ演算由 Alonzo Church 和 Stephen Cole Kleene 在 20 世纪三十年代引入,Church 运用 lambda 演算在 1936 年给出 判定性问题 (Entscheidungsproblem) 的一个否定的答案。这种演算可以用来清晰地定义什么是一个可计算函数。关于两个 lambda 演算表达式是否等价的命题无法通过一个通用的算法来解决,这是不可判定性能够证明的头一个问题,甚至还在停机问题之先。Lambda 演算对函数式编程有巨大的影响,特别是Lisp 语言。

    Church 整数

    在 lambda 演算中有许多方式都可以定义自然数,但最常见的还是Church 整数,下面是它们的定义:

    $$ 0 = \lambda f\cdot \lambda x\cdot x $$
    $$ 1 = \lambda f\cdot \lambda x\cdot f x $$
    $$ 2 = \lambda f\cdot \lambda x\cdot f (f x ) $$
    $$ 3 = \lambda f\cdot \lambda x\cdot f(f (f x )) $$
    $$...$$
    $$ n = \lambda f\cdot \lambda x\cdot f^n( x ) $$

    以此类推。直观地说,lambda 演算中的数字 n 就是一个把函数 f 作为参数并以 f 的 n 次幂为返回值的函数。换句话说,Church 整数是一个高阶函数 :

     以单一参数函数 f 为参数,返回另一个单一参数的函数。
    

    (注意在 Church 原来的 lambda 演算中,lambda 表达式的形式参数在函数体中至少出现一次,这使得我们无法像上面那样定义 0)

    C#Lambda表达式

    C#的Lambda 表达式都使用 Lambda 运算符 =>,该运算符读为“goes to”。语法如下:

    形参列表=>函数体
    (input parameters) => expression
    (input parameters) => {statement;}
    
    

    函数体多于一条语句的可用大括号括起。例子:

    () => Console.Write("0个参数")
    I => Console.Write("1个参数时参数列中可省略括号,值为:{0}",i)
    (x,y) => Console.Write("包含2个参数,值为:{0}*{1}",x,y)
    
    //两条语句时必须要大括号
    I => { i++;Console.Write("两条语句的情况"); }
    
    

    Lambda 表达式是一种可用于创建委托或表达式目录树类型的匿名函数。 通过使用 lambda 表达式,可以写入可作为参数传递或作为函数调用值返回的本地函数。 Lambda 表达式对于编写 LINQ 查询表达式特别有用。

    若要创建 Lambda 表达式,需要在 Lambda 运算符 => 左侧指定输入参数(如果有),然后在另一侧输入表达式或语句块。 例如,lambda 表达式 x => x * x 指定名为 x 的参数并返回 x 的平方值。

    过滤条件:

    
    List<User> users = new List<User>();
    Func<User, bool> predicate = ((user) =>
        {
            return user.UserId > 100;
        }
    );
    List<User> temps = users.Where(predicate).ToList();
    
    

    Java Lambda表达式

    Java 8的一个大亮点是引入Lambda表达式,使用它设计的代码会更加简洁。当开发者在编写Lambda表达式时,也会随之被编译成一个函数式接口。下面这个例子就是使用Lambda语法来代替匿名的内部类,代码不仅简洁,而且还可读。
    没有使用Lambda的老方法:

    Runnable r = new Runnable(){
    @Override
    public void run(){
        System.out.println("Running without Lambda");
    }
    };
    

    使用Lambda表达式:

    Runnable r =() -> {
        System.out.println("Running without Lambda");
    };
    
    
    

    C++ Lambda表达式

    ISO C++ 11 标准的一大亮点是引入Lambda表达式。基本语法如下:

    [capture list] (parameter list) ->return type { function body }
    

    其中除了“[ ]”(其中捕获列表可以为空)和“复合语句”(相当于具名函数定义的函数体),其它都是可选的。它的类型是唯一的具有成员operator()的非联合的类类型,称为闭包类型(closure type)。
    C++中,一个lambda表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。它与普通函数不同的是,lambda必须使用尾置返回来指定返回类型。

    scala的匿名函数

    scala的匿名函数使用非常的广泛,这也是函数式语言的标志之一。

    scala中的集合类List有一个map方法,该方法接收一个函数作为参数,对List中的每一个元素使用参数指定的方法,此处非常适合用匿名函数,请看下面代码:

    val l=List(1,2,3)
    l.map(i=>i+9)
    
    

    上面代码的第二行map函数的参数就是一个匿名函数,或者是lambda表达式。

    i=>i+9中的=>是参数列表和返回值的分隔符,如果少于两个参数可以不写小括号,后面部分是函数的返回值。如果函数体包含多行代码可以使用花括号,例如:

    
    l.map((i)=>{
      println("HI");
      i+9
    })
    
    

    scala函数特有特性之调配curried

    定义常规的scala add函数:

    
    def add (i:Int,j:Int):Int=i+j
    
    

    这个函数太简单了.

    我们定义一个devide函数实现除法,一般我们会这么写:

    
    // 多参数的写法
    def multiply(x: Int, y: Int) = x * y
    
    
    

    Moses Schnfinkel 和 Gottlob Frege 发明了如下的表达方式:

    
    def multiply(x: Int)(y: Int) => x * y
    
    

    这个函数和上面的函数不同之处在于它的参数列表,是一个参数一个小括号,不是把所有参数都放到一个括号内的。下面我们通过实际的例子来理解scala的curried。

    我们要定义一个乘法函数:

    
    // 柯里化的写法
    def multiply(x: Int)(y: Int) = x * y
    
    // 计算两个数的乘积
    multiply(6)(7)
     
    
    

    multiply(6)返回的是函数(y: Int)=>6*y,再将这个函数应用到7

    
    (6 * y ) (7) = 42
    
    

    最终得到结果42.

    这就是scala的乘法函数了.

    curried不太好理解,enjoy it!函数柯里化的主要功能是提供了强大的动态函数创建方法,通过调用另一个函数并为它传入要柯里化(currying)的函数和必要的参数而得到。通俗点说就是利用已有的函数,再创建一个动态的函数,该动态函数内部还是通过已有的函数来发生作用.

    我们在想, 这些计算机科学家,数学家们脑子里天天都在想些什么? 尽发明一些普通人不好理解的概念. 但是, 存在即合理.既然这玩意存在着,必定表明必有其存在之意义.

    多参数是个虚饰,不是编程语言的根本性的特质。利用柯里化把某个函数参数单独拎出来,提供更多用于类型推断的信息.

    在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。

    在直觉上,柯里化声称“如果你固定某些参数,你将得到接受余下参数的一个函数”。所以对于有两个变量的函数yx,如果固定了 y = 2,则得到有一个变量的函数 2x。
    在理论计算机科学中,柯里化提供了在简单的理论模型中比如只接受一个单一参数的lambda 演算中研究带有多个参数的函数的方式。

    柯里化特性决定了它这应用场景。提前把易变因素,传参固定下来,生成一个更明确的应用函数。最典型的代表应用,是bind函数用以固定this这个易变对象。

    
    Function.prototype.bind = function(context) {
        var _this = this,
        _args = Array.prototype.slice.call(arguments, 1);
        return function() {
            return _this.apply(context, _args.concat(Array.prototype.slice.call(arguments)))
        }
    }
    
    

    柯里化其实本身是固定一个可以预期的参数,并返回一个特定的函数。这增加了函数的适用性,但同时也降低了函数的适用范围。这个思路有点像微积分里面的多重积分的处理方式.

    柯里化的本质就是函数参数单一化. 因为多参数有其问题.你在写很多高阶函数的时候,你都得考虑你的 参数函数f 在面临各种传参方式的情况下要怎么处理,这事儿是痛苦并且容易忘记的,因为你本不应该关心f的参数是些啥的!盲目地引入传递多参的机制,其实是没有把问题想明白的表现。语法层面支持的多参函数定义是一种扁平、简单、有效的方式,但是,当程序需要做高阶函数抽象的时候,这种方式会带来麻烦。

    通过组合型数据来传递复杂参数,是一种很自然的方式,这种情况下并不需要刻意考虑“多参函数”这种问题,并且这种方式对于做高阶函数抽象非常友好。

    科里化的方式定义多参函数,同样是一种自然产生的方式,只要把函数当做一等公民,就会存在科里化的方式。科里化的方式使得我们的函数可以更细粒度地方便地组合。

    但是,是否要使用科里化的方式,却是一件需要细细揣摩的事情。比如,我要定义一个rotate函数,它接受一个二维向量的坐标,返回一个逆时针旋转九十度后的向量坐标,那么你可能会把它定义成

    
    rotate = (\x -> (\y -> (y, -x)))
    
    

    或者

    
    rotate = (\(x, y) -> (y, -x))
    
    

    你觉得哪种定义方式比较好呢? 在这个例子里,显然是第二种定义方式比较好,

    • 一来,二维向量的两个坐标通常是成对出现的,很少会有“部分应用”的需要,所以也就自然用不到科里化的任何好处;
    • 二来,当你需要把一个向量rotate两次的时候,科里化版本就会体现出异常的麻烦了。

    所以,结论是:

        
        如果某些参数在大部分情况下,总是需要同时给出,才具有真实的意义,那么应该把这些参数组合成一个组合型参数来处理,而不应该科里化。
        如果要定义的多参函数是一个闭合函数,那么它是很可能需要被多次应用的,这种情况下,应该用组合型参数的方式来处理。
        如果先指定某一些参数就有明确的意义,那么就应该用科里化的方式来处理。
    
    

    在F#里,各种函数的signature 都是 int -> int -> int ->什么的……
    what does that even mean??
    Currying和partial application对于functional programming有怎样的真正的威力?

    currying和partial application会affect performance吗?

    匿名函数

    函数不一定需要名称:

    
    (x: Double) => 3 * x  // 该匿名函数将传给它的参数乘3
    
    

    可以将匿名函数赋值给变量,也可以当参数传递。

    高阶函数(Higher-order function)

    变量可以指向函数

    以Python内置的求绝对值的函数abs()为例,调用该函数用以下代码:

    abs(-10)
    10
    

    但是,如果只写abs呢?

    abs
    <built-in function abs>
    

    可见,abs(-10)是函数调用,而abs是函数本身。

    要获得函数调用结果,我们可以把结果赋值给变量:

     x = abs(-10)
     x
    10
    
    

    但是,如果把函数本身赋值给变量呢?

    
    f = abs
    f
    <built-in function abs>
    
    

    结论:函数本身也可以赋值给变量,即:变量可以指向函数。

    如果一个变量指向了一个函数,那么,可否通过该变量来调用这个函数?用代码验证一下:

    
    f = abs
    f(-10)
    10
    
    

    成功!说明变量f现在已经指向了abs函数本身。

    函数名也是变量

    那么函数名是什么呢?函数名其实就是指向函数的变量!对于abs()这个函数,完全可以把函数名abs看成变量,它指向一个可以计算绝对值的函数!

    高阶函数

    既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。

    一个最简单的高阶函数:

    
    def add(x, y, f):
        return f(x) + f(y)
        
        
    

    当我们调用add(-5, 6, abs)时,参数x,y和f分别接收-5,6和abs,根据函数定义,我们可以推导计算过程为:

    
    x ==> -5
    y ==> 6
    f ==> abs
    f(x) + f(y) ==> abs(-5) + abs(6) ==> 11
    
    
    

    用代码验证一下:

    add(-5, 6, abs)
    11
    
    

    编写高阶函数,就是让函数的参数能够接收别的函数。

    把函数作为参数传入,这样的函数称为高阶函数,函数式编程就是指这种高度抽象的编程范式。


    <script type="text/javascript" src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=default"></script>
    

    相关文章

      网友评论

      • lambeta:> 如果要定义的多参函数是一个闭合函数,那么它是很可能需要被多次应用的,这种情况下,应该用组合型参数的方式来处理。

        这句话中闭合函数怎么理解,为何说它很可能需要被多次应用?

      本文标题:函数式编程与面向对象编程[1]: Lambda表达式 函数柯里化

      本文链接:https://www.haomeiwen.com/subject/ywblrttx.html