Currying 的局限性

很多基于 lambda calculus 的程序语言,比如 ML 和 Haskell,都习惯用一种叫做 currying 的手法来表示函数。比如,如果你在 Haskell 里面这样写一个函数:

f x y = x + y

然后你就可以这样把链表里的每个元素加上 2:

map (f 2) [1, 2, 3]

它会输出 [3, 4, 5]

注意本来 f 需要两个参数才能算出结果,可是这里的 (f 2) 只给了 f 一个参数。这是因为 Haskell 的函数定义的缺省方式是“currying”。Currying 其实就是用“单参数”的函数,来模拟多参数的函数。比如,上面的 f 的定义在 Scheme 里面相当于:

(define f
(lambda (x)
(lambda (y)
(+ x y))))

它是说,函数 f,接受一个参数 x,返回另一个函数(没有名字)。这个匿名函数,如果再接受一个参数 y,就会返回 x + y。所以上面的例子里面,(f 2) 返回的是一个匿名函数,它会把 2 加到自己的参数上面返回。所以把它 map 到 [1, 2, 3],我们就得到了 [3, 4, 5]

在这个例子里面,currying 貌似一个挺有用的东西,它让程序变得“简短”。如果不用 currying,你就需要制造另一个函数,写成这个样子:

map (\y->f 2 y) [1, 2, 3]

这就是为什么 Haskell 和 ML 的程序员那么喜欢 currying。这个做法其实来源于最早的 lambda calculus 的设计。因为 lambda calculus 的函数都只有一个参数,所以为了能够表示多参数的函数,有一个叫 Haskell Curry 的数学家和逻辑学家,发明了这个方法。

当然,Haskell Curry 是我很尊敬的人。不过我今天想指出的是,currying 在程序设计的实践中,其实并不是想象中的那么好。大量使用 currying,其实会带来程序难以理解,复杂性增加,并且还可能因此引起意想不到的错误。

不用 currying 的写法(\y->f 2 y)虽然比起 currying 的写法(f 2)长了那么一点,但是它有一点好。那就是你作为一个人(而不是机器),可以很清楚的从“\y->f 2 y”这个表达式,看到它的“用意”是什么。你会很清楚的看到:

“f 本来是一个需要两个参数的函数。我们只给了它第一个参数 2。我们想要把 [1, 2, 3] 这个链表里的每一个元素,放进 f 的第二个参数 y,然后把 f 返回的结果一个一个的放进返回值的链表里。”

仔细看看上面这段话说了什么吧,再来看看 (f 2) 是否表达了同样的意思?注意,我们现在的“重点”在于你,一个人,而不在于计算机。你仔细想,不要让思维的定势来影响你的判断。

你发现了吗?(f 2) 并不完全的含有 \y->f 2 y 所表达的内容。因为单从 (f 2) 这个表达式(不看它的定义),你看不到“f 总共需要几个参数”这一信息,你也看不到 (f 2) 会返回什么东西。f 有可能需要2个参数,也有可能需要3个,4个,5个…… 比如,如果它需要3个参数的话,map (f 2) [1, 2, 3] 就不会返回一个整数的链表,而会返回一个函数的链表,它看起来是这样:[(\z->f 2 1 z), (\z->f 2 2 z), (\z->f 2 3 z)]。这三个函数分别还需要一个参数,才会输出结果。

这样一来,表达式 (f 2) 含有的对“人”有用的信息,就比较少了。你不能很可靠地知道这个函数接受了一个参数之后会变成什么样子。当然,你可以去看 f 的定义,然后再回来,但是这里有一种“直觉”上的开销。如果你不能同时看见这些信息,你的脑子就需要多转一道弯,你就会缺少一些重要的直觉。这种直觉能帮助你写出更好的程序。

然而,currying 的问题不止在于这种“认知”的方面,有时候使用 curry 会直接带来代码复杂性的增加。比如,如果你的 f 定义不是加法,而是除法:

f x y = x / y

然后,我们现在需要把链表 [1, 2, 3] 里的每一个数都除以 2。你会怎么做呢?

map (f 2) [1, 2, 3] 肯定不行,因为 2 是除数,而不是被除数。熟悉 Haskell 的人都知道,可以这样做:

map (flip f 2) [1, 2, 3]

flip 的作用是“交换”两个参数的位置。它可以被定义为:

flip f x y = f y x

但是,如果 f 有 3 个参数,而我们需要把它的第 2 个参数 map 到一个链表,怎么办呢?比如,如果 f 被定义为:

f x y z = (x - y) / z

稍微动一下脑筋,你可能会想出这样的代码:

map (flip (f 1) 2) [1, 2, 3]

能想出这段代码说明你挺聪明,可是如果你这样写代码,那就是缺乏一些“智慧”。有时候,好的程序其实不在于显示你有多“聪明”,而在于显示你有多“笨”。现在我们就来看看笨一点的代码:

map (\y -> f 1 y 2) [1, 2, 3]

现在比较一下,你仍然觉得之前那段代码很聪明吗?如果你注意观察,就会发现 (flip (f 1) 2) 这个表达式,是多么的晦涩,多么的复杂。

(flip (f 1) 2) 里面,你几乎看不到自己想要干什么。而 \y-> f 1 y 2 却很明确的显示出,你想用 1 和 2 填充掉 f 的第一,三号参数,把第二个参数留下来,然后把得到的函数 map 到链表 [1, 2, 3]。仔细看看,是不是这样的?

所以你花费了挺多的脑力才把那使用 currying 的代码写出来,然后你每次看到它,还需要耗费同样多的脑力,才能明白你当时写它来干嘛。你是不是吃饱了没事干呢?

练习题:如果你还不相信,就请你用 currying 的方法(加上 flip)表达下面这个语句,也就是把 f 的第一个参数 map 到链表 [1, 2, 3]

map (\y -> f y 1 2) [1, 2, 3]

得到结果之后再跟上面这个语句对比,看谁更加简单?

到现在你也许注意到了,以上的“笨办法”对于我们想要 map 的每一个参数,都是差不多的形式;而使用 currying 的代码,对于每个参数,形式有很大的差别。所以我们的“笨办法”其实才是以不变应万变的良策。

才三个参数,currying 就显示出了它的弱点,如果超过三个参数,那就更麻烦了。所以很多人为了写 currying 的函数,特意把参数调整到方便 currying 的顺序。可是程序的设计总是有意想不到的变化。有时候你需要增加一个参数,有时候你又想减少一个参数,有时候你又会有别的用法,导致你需要调整参数的顺序…… 事先安排好的那些参数顺序,很有可能不能满足你后来的需要。即使它能满足你后来的需要,你的函数也会因为 currying 而难以看懂。

这就是为什么我从来不在我的 ML 和 Haskell 程序里使用 currying 的原因。古老而美丽的理论,也许能够给我带来思想的启迪,可是未必就能带来工程中理想的效果。

【转】Currying 的局限性的更多相关文章

  1. 前端开发者进阶之函数柯里化Currying

    穆乙:http://www.cnblogs.com/pigtail/p/3447660.html 在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接 ...

  2. 函数式中的 currying

    currying 是函数式语言中经常遇到的一个概念,翻译成 柯里化,不是库里化. currying 指的是将接收多个参数的函数变换成接收一个单一参数,并且返回接收余下的参数而且返回结果的新函数的技术. ...

  3. 函数柯里化(Currying)示例

    ”函数柯里化”是指将多变量函数拆解为单变量的多个函数的依次调用, 可以从高元函数动态地生成批量的低元的函数.可以看成一个强大的函数工厂,结合函数式编程,可以叠加出很BT的能力.下面给出了两个示例,说明 ...

  4. 并发编程 12—— 任务取消与关闭 之 shutdownNow 的局限性

    Java并发编程实践 目录 并发编程 01—— ThreadLocal 并发编程 02—— ConcurrentHashMap 并发编程 03—— 阻塞队列和生产者-消费者模式 并发编程 04—— 闭 ...

  5. 【EF 译文系列】重试执行策略的局限性(EF 版本至少为 6)

    原文链接:Limitations with Retrying Execution Strategies (EF6 onwards) 当使用重试执行策略的时候,大体有以下两种局限性: 不支持以流的方式进 ...

  6. [实变函数]5.1 Riemann 积分的局限性, Lebesgue 积分简介

    1 Riemann 积分的局限性 (1) Riemann 积分与极限的条件太严:    $$\bex    f_k\rightrightarrows f\ra \lim \int_a^b f_k   ...

  7. JAVA与多线程开发(线程基础、继承Thread类来定义自己的线程、实现Runnable接口来解决单继承局限性、控制多线程程并发)

    实现线程并发有两种方式:1)继承Thread类:2)实现Runnable接口. 线程基础 1)程序.进程.线程:并行.并发. 2)线程生命周期:创建状态(new一个线程对象).就绪状态(调用该对象的s ...

  8. JavaScript中的Partial Application和Currying

    这篇文章是一篇学习笔记,记录我在JS学习中的一个知识点及我对它的理解,知识点和技巧本身并不是我原创的.(引用或参考到的文章来源在文末) 先不解释Partial Application(偏函数应用)和C ...

  9. android TabActivity的局限性 是否还有存在的必要性

     TabActivity的局限性 是否还有存在的必要性 其实谷歌有此举动,我们也应该早就想到了,为什么会这么说呢?那就要从TabActivity的原理开始说起了. 做个假定先: 比如我们最外面的Act ...

随机推荐

  1. LCX端口内网映射转发

    这几天在渗透一家奶茶店的时候, 使用溢出攻击获得了奶茶店shell, 截屏以后发现该电脑有安装sqlserver, 但是数据库无法远程连接, 所以我决定使用端口映射工具突破端口连接限制: 我的机器地址 ...

  2. 使用windbg抓取崩溃文件和分析的过程

    在软件编程中,崩溃的场景比较常见的.且说微软技术再牛X,也是会出现崩溃的场景.网上有一段Win98当着比尔盖茨蓝屏的视频非常有意思. (转载请指明出于breaksoftware的csdn博客)     ...

  3. Android 手把手带你玩转自己定义相机

    本文已授权微信公众号<鸿洋>原创首发,转载请务必注明出处. 概述 相机差点儿是每一个APP都要用到的功能,万一老板让你定制相机方不方?反正我是有点方. 关于相机的两天奋斗总结免费送给你. ...

  4. achartengine(Google给android提供的画图工具包)的介绍和使用

    AChartEngine(ACE)是Google为Android提供的一个开源绘制工具包.它集成了绘制多种图形的功能:折线图.散点图.气泡图.柱状图.饼图.仪表图等图形. 下载地址:http://do ...

  5. Atlas系列一:Atlas功能特点FAQ

    1:Atlas是否支持多字符集? 支持,可以在test.cnf中指定. #默认字符集,设置该项后客户端不再需要执行SET NAMES语句charset = utf8 2:Atlas是否支持事物操作? ...

  6. js instanceof 实现原理

    function instanceof(left, right) { // 获得类型的原型 let prototype = right.prototype // 获得对象的原型 left = left ...

  7. dpkg: warning: files list file for package `*****' missing, assuming package has no files currently installed解决办法

    一个好友的国外VPS由于操作不当,结果装软件的时候总是提示dpkg: warning: files list file for package `*****' missing, assuming pa ...

  8. NDK编译常用命令及不同版本so编译方法

    工具说明 addr2line 把程序地址转换为文件名和行号.在命令行中给它一个地址和一个可执行文件名,它就会使用这个可执行文件的调试信息指出在给出的地址上是哪个文件以及行号. ar 建立.修改.提取归 ...

  9. cocos2d-js 入门 (主要是HTML5)

    cocos2d-js是cocos2d-x的JavaScript版本,真正跨全平台的游戏引擎,采用原生JavaScript语言,可发布到包括Web平台,iOS,Android,Windows Phone ...

  10. Java AtomicBoolean (Java代码实战-008)

    值得一提的是,Java的AtomXXX类并不是使用了锁的方式进行同步,而是采用了一种新的理念,叫做CAS(Compare And Swap)CAS是一组CPU原语指令,用来实现多线程下的变量同步(原子 ...