Wushuang’blog


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

谈语法

发表于 2016-03-15   |   分类于 语法   |  

谈语法


使用和研究过这么多程序语言之后,我觉得几乎不包含多余功能的语言,只有一个:Scheme。所以我觉得它是学习程序设计最好的入手点和进阶工具。当然 Scheme 也有少数的问题,而且缺少一些我想要的功能,但这些都瑕不掩瑜。在用了很多其它的语言之后,我觉得 Scheme 真的是非常优美的语言。

阅读全文 »

谈谈 Currying

发表于 2016-03-15   |  

谈谈 Currying

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

1
f x y = x + y

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

1
2
3
<!--more-->

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

它会输出 [3, 4, 5]。

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

1
2
3
4
(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,你就需要制造另一个函数,写成这个样子:

1
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 定义不是加法,而是除法:

1
f x y = x / y

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

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

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

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

1
flip f x y = f y x

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

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

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

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

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

1
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]:

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

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

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

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

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

谈惰性求值

发表于 2016-03-15   |  

谈惰性求值

从之前的几篇博文里面你也许已经看到了,Haskell 其实是问题相当严重的语言,然而这些问题却没有引起足够的重视。我能看到的 Haskell 的问题在于:

阅读全文 »

谈程序的“通用性”

发表于 2016-03-15   |  

谈程序的“通用性”

在现实的软件工程中,我经常发现这样的一种现象。本来用很简单的代码就可以解决的问题,却因为设计者过分的关注了“通用性”,“可维护性”和“可扩展性”,被搞得绕了几道弯,让人琢磨不透。

阅读全文 »

谈“测试驱动的开发”

发表于 2016-03-15   |  

谈“测试驱动的开发”

现在的很多公司,包括 Google 和我现在的公司 Coverity,都喜欢一种“测试驱动的开发”(test-driven development)。它的原理是,在写程序的时候同时写上自动化的“单元测试”(unit test)。在代码修改之后,这些测试可以批量的被运行,这样就可以避免不应该出现的错误。

阅读全文 »

什么是语义学

发表于 2016-03-15   |   分类于 语义学   |  

什么是语义学

很多人问我如何在掌握基本的程序语言技能之后进入“语义学”的学习。现在我就简单介绍一下什么是“语义”,然后推荐一本入门的书。这里我说的“语义”主要是针对程序语言,不过自然语言里的语义,其实本质上也是一样的。

阅读全文 »

谈 Linux,Windows 和 Mac

发表于 2016-03-15   |  

谈 Linux,Windows 和 Mac

这段时间受到很多人的来信。他们看了我很早以前写的推崇 Linux 的文章,想知道如何“抛弃 Windows,学习 Linux”。天知道他们在哪里找到那么老的文章,真是好事不出门…… 我觉得我有责任消除我以前的文章对人的误导,洗清我这个“Linux 狂热分子”的恶名。我觉得我已经写过一些澄清的文章了,可是怎么还是有人来信问 Linux 的问题。也许因为感觉到“舆论压力”,我把文章都删了。

简言之,我想对那些觉得 Linux 永远也学不会的“菜鸟”们说:

阅读全文 »

什么是启发

发表于 2016-03-15   |  

什么是启发

我喜欢用“启发”这个词。比如我经常会对人说:“你启发了我。”然而听到这话的人有时候不明白我的意思,自以为高我一筹,于是顿显傲气。其实我用“启发”这个词,是有深刻含义的。“启发”的意思并不等于“我没有你懂得多”或者“你比我聪明”,而是一个很含糊的词。

如果 A 受到了 B 启发,有几种可能性:

阅读全文 »

什么是“脚本语言”

发表于 2016-03-15   |  

什么是“脚本语言”

很多人都会用一些“脚本语言”(scripting language),却很少有人真正的知道到底什么是脚本语言。很多人用 shell 写一些“脚本”来完成日常的任务,用 Perl 或者 sed 来处理一些文本文件,很多公司用“脚本”来跑它们的“build”(叫做 build script)。那么,到底什么是“脚本语言”与“非脚本语言”的区别呢?

阅读全文 »

什么是“对用户友好”

发表于 2016-03-15   |  

什么是“对用户友好”

当我提到一个工具“对用户不友好”(user-unfriendly)的时候,我总是被人“鄙视”。难道这就叫“以其人之道还治其人之身”?想当年有人对我抱怨 Linux 或者 TeX 对用户不友好的时候,我貌似也差不多的态度吧。现在当我指出 TeX 的各种缺点,提出新的解决方案的时候,往往会有美国同学眼角一抬,说:“菜鸟们抱怨工具不好用,那是因为他们不会用。LaTeX 是‘所想即所得’,所以不像 Word 之类的上手。”

阅读全文 »
12345
Wu Shuang

Wu Shuang

胡编一通,乱写一气。猴赛雷。

50 日志
16 分类
12 标签
RSS
Weibo Twitter GitHub 简书 王垠 Node中文 阮一峰 前端乱炖 淘宝前端 腾讯前端 w3ctech 前端观察 开源中国 Fedora社区 大前端 蓝色理性 css88 汤姆大叔 夏天的森林 前端导航 Js秘密花园 徐飞 Div.IO 极客学院 妙味课堂 慕课网 智能社 传智播客 notes
© 2016 Wu Shuang
由 Hexo 强力驱动
主题 - NexT.Pisces