如何成为一名函数式程序员( I)

2016-11-18 10:11:59 +08:00
 lfj

理解函数式编程概念是最重要,有时也是最难的一步。

**学开车

在第一次学开车的时候,挣扎是难免的。看着别人开起来很简单,但实际上比我们想象的要难一些。在爸妈的车里练习,直到对小区的路都熟悉了才敢开到路上去。在经过反复的练习以及一些恐慌的时刻之后,我们终于学会了开车,拿到了驾照。

拿到驾照之后,我们会抓住所有的机会开车出去。一次一次,越来越娴熟,我们也越来越自信。有一天我们不得不借别人的车出去,或者我们的车彻底坏了,不得不买一辆新的。这时候问题就来了,开一辆新车是怎样的感觉?会和第一次开车的感觉一样吗?

*这两种感觉差老远了

第一次开车的时候,完全是陌生的感觉。虽然在此之前,我们坐过车,但都是以乘客的身份。开车就不一样了,我们是坐在驾驶位子上,控制车里的各种东西。

在开第二辆车的时候,我们只是会问一些简单的问题,比如钥匙去哪了?灯在哪里?你怎么使用转向灯,怎么调侧视镜?

之后就非常顺利了。但是为什么这次就这么简单呢?

这是因为新车和旧车很相像,基本的东西都一样,几乎在同样的位置。

有些东西安装的不一样,可能增添了额外的特性( feature ),在第一次甚至第二次开的时候都不会用到。最后我们也都了解了这些新特性,至少了解了我们关心的那些。

学习编程语言就有点像学车。第一次最难,但是一旦你学会了一种,之后的就简单了。

当你开始学习第二种语言的时候,你会问一些这样问题,比如“我如何创建一个模块?你怎么搜索数组? substring 函数的参数是多少?”

在学习新语言的时候,你会变得自信,因为你会想到之前的语言,加了一些新的东西,学起来也容易些。

**你的第一艘宇宙飞船


不管你这辈子是只开过一辆车还是开了几十辆车,现在想象一下你要驾驶一艘宇宙飞船的情景。

如果你要驾驶飞船的话,你肯定不会期望曾经开车的能力能够帮到你。你得从零开始。(作为程序员,我们都是从 0 开始计数。)

开始训练的时候,你就已预想到在太空中是完全不一样的,虽然物理空间不变,还是在同一宇宙中,但是驾驶飞船和开车完全是两码事。

这和学习函数式编程是类似的。编程就是在思考,函数式编程会教你如何以不同的方式思考。以至于,你之后再也没法回到以前的思考方式。

**将自己归零

人们很喜欢说这句话,将自己归零,这么说是有些道理的。学习函数式编程就像是从头开始。不完全,但很贴切。如果你是希望一切都从头学起的话,那是最好的。

看问题的角度正确了,才会有正确的预期;预期正确了,才不会在遇到难题的时候轻易放弃。

作为一名程序员,有各种各样的事情,你已成习惯了,但是在函数式编程的时候是没法做的。

就像在车里,你习惯从私人车道倒出来。但是在飞船里,根本就没有倒挡。现在你可能会想,“什么?没倒挡?!没倒挡,我 TM 怎么开?!”

没倒挡就说明在飞船里根本不需要倒挡,因为飞船可以在三维空间里任人操作。一旦你理解了这点之后,你就不会再想着倒挡了。事实上,某一天,你会觉得车这东西真的是限制太多了。

*学习函数式编程需要一段时间,要有耐心。

走出命令式编程的冰冷世界,温柔地沉浸到函数式编程的暖春中。

在开始研究函数语言之前,下面的函数式编程概念对你会有所帮助。如果你已经开始尝试了,下面的编程概念会让你有更全面的理解。

请花点时间向下读,理解一下编码示例。 最重要的就是你要理解它。

**纯洁性

当函数型程序员说起纯度 纯洁性的时候,他们指的是纯函数。纯函数是非常简单的函数,只运行输入的参数。

举一个用 Javascript 写的纯函数例子:

var z = 10;
function add(x, y) {
    return x + y;
}

要注意的是add函数不会触发z变量,不会从z开始读取,也不会写到z,只会读取xy(输入值),返回加和结果。

这就是纯函数。如果add函数访问到z,那么它就不是纯函数了。

下面是另一个函数:

function justTen() {
    return 10;
}

如果函数just Ten是纯函数的话,那它只能返回一个常数。为什么?

因为我们还没输入任何东西。如果要变成一个纯函数的话,是没法访问到输入值以外的任何值的,所以唯一能返回的值就是个常数。

没有参数的纯函数无法运行,所以没太大用处。如果just Ten定义为一个常数,那就更好了。

大多数有用的纯函数都必须至少有一个参数。

看看这个函数:

function addNoReturn(x, y) {
    var z = x + y
}

这个函数不能返回任何值。添加xy,得出变量z,但是不会返回任何值。

这是个纯函数,因为它只处理输入值。但是输入了也没有返回任何结果,所以这函数是无效的。

所有有效的纯函数都必须返回一些值才行。

我们再看看第一个 add 函数:

function add(x, y) {
    return x + y;
}
console.log(add(1, 2)); // prints 3
console.log(add(1, 2)); // still prints 3
console.log(add(1, 2)); // WILL ALWAYS print 3

我们看到add(1,2)总是返回 3 。这并不奇怪,因为这个函数是纯函数。如果add函数使用外部的值,那么就没法预知结果了。

*同样的输入,纯函数得出的结果都是一样的。

因为纯函数是没法改变任何外部变量的,所以下面这些函数都不是纯函数:

writeFile(fileName);
updateDatabaseTable(sqlCmd);
sendAjaxRequest(ajaxRequest);
openSocket(ipAddress);

这些函数都有副作用,当你调用的时候,文件和数据表都会改变,数据发送到服务器,或者调用 OS 套接,不仅仅是运行输入值返回输出值。所以永远也没法预判这些函数会返回什么东西。

*纯函数没有副作用。

在命令式编程语言中,比如 Javascript 、 Java 和 C#,副作用无处不在。这使得调试非常困难,因为在项目中一个变量随处都可改变。所以当发现一个 bug ,这 bug 是因为一个变量在错误的时间被改成了错误的数值导致的,怎么找出来?到处找吗?根本不行!

这时候,你可能会在想 “只有个纯函数,我 TM 怎么办?”

在函数式编程中,你不仅仅是写纯函数。

函数式编程无法彻底消除副作用,只能限定。因为各个项目要和真实的世界交互,每一个项目中某一部分必须是非纯函数的。目标是最小化非纯码的数量,将其与其它的项目隔离开。

**不变性

你还记得你第一次看到下面这些代码的时候吗?

var x = 1;
x = x + 1;

是不是每个教你的人都告诫你忘记你在数学课上学的东西?在数学上,x是不可能等于x+1的。

但是在命令式编程中,x+1的意思是将x现在的值加上1,然后将值返回给x

在函数式编程中,x = x + 1是非法的。所以你必须得记住你忘记的那一点点数学知识。

*函数式编程中不存在变量。

存储的值也叫作变量,但是他们都是常数,比如一旦 x 取了一个值,那它永远就是这个值。

不用担心, x 通常是个局部变量,所以它的生命通常很短暂。但是在生命期中,它的值是无法改变的。

下面是 EIm 中的一个固定变量的例子, EIm 是一种用于网页开发的纯函数式编程语言。

addOneToSum y z =
    let
        x = 1
    in
        x + y + z

如果你对 ML-Style 语法不熟悉的话,我来解释一下,addOneToSum是一个有两个参数(yz)的函数。

let模块内,x绑定值为 1 ,也就是它的值等于 1 。在函数退出或者更精确的说是当let模块求值之后,x的生命就结束了。

in模块,计算包括let模块定义的数值,也就是x。返回x + y + z的计算结果,更精确地说是,因为x = 1,返回1 + y + z这样一个结果。

你肯定会又问“没有变量,我 TM 到底该怎么做?!”

我们来想想什么时候要调整变量。一般有两种情况:多值变化(比如:改变某个对象或记录的一个值)和单值变化(比如:循环计数器)。

函数式编程通过利用数据结构复制值变化后的记录(不用复制全部记录)的方式高效地处理记录中数值的变化。

函数式编程也是通过同样地方式(即复制)解决单值变化。

是的,但是没有循环。

你肯定又抓狂了:“什么?没变量,现在又没循环?!”

冷静,不是不能进行循环,只是没有特定的循环结构,像for, while, do, repeat等。

函数式编程使用递归循环

下面是使用 Javascript 语言写循环的两种方式:

// simple loop construct
var acc = 0;
for (var i = 1; i <= 10; ++i)
    acc += i;
console.log(acc); // prints 55
 // without loop construct or variables (recursion)
function sumRange(start, end, acc) {
     if (start > end)
         return acc;
     return sumRange(start + 1, end, acc + start)
}
 console.log(sumRange(1, 10, 0)); // prints 55

我们看看递归(一种函数方法)是怎样通过自我调用一个 new 开始(start + 1) 和一个 new 累加器(acc + start)实现和 for 循环一样的结果的。递归不需要改变原先的值,而是使用旧值算出的新值。

不幸的是,即使你花了一点时间学习,在 Javascript 中也很难看到这个。有两个原因:一是 Javascript 语法比较杂乱;二是你可能不习惯用递归的方式思考。

在 EIm 中,读取更容易,理解也相应的更容易:

sumRange start end acc =
    if start > end then  
        acc
    else
        sumRange (start + 1) end (acc + start) 

下面是运行过程:

sumRange 1 10 0 =      -- sumRange (1 + 1)  10 (0 + 1)
sumRange 2 10 1 =      -- sumRange (2 + 1)  10 (1 + 2)
sumRange 3 10 3 =      -- sumRange (3 + 1)  10 (3 + 3)
sumRange 4 10 6 =      -- sumRange (4 + 1)  10 (6 + 4)
sumRange 5 10 10 =     -- sumRange (5 + 1)  10 (10 + 5)
sumRange 6 10 15 =     -- sumRange (6 + 1)  10 (15 + 6)
sumRange 7 10 21 =     -- sumRange (7 + 1)  10 (21 + 7)
sumRange 8 10 28 =     -- sumRange (8 + 1)  10 (28 + 8)
sumRange 9 10 36 =     -- sumRange (9 + 1)  10 (36 + 9)
sumRange 10 10 45 =    -- sumRange (10 + 1) 10 (45 + 10)
sumRange 11 10 55 =    -- 11 > 10 => 55
55

你可能会觉得 for 循环更容易理解。然而,这是有争议的,更可能是熟悉度的问题,非递归循环需要可变性,可变性这就糟糕了。

这里我没有完全解释不变性的好处,你可以到另一篇文章《为什么程序员需要限定》( Why Programmers Need Limits )中的 Global Mutable State 查看一下,会学到更多。

不变性一个明显的好处就是如果你访问了项目中的一个值,你只能读取访问,这意味着其他没有人能改变这个值,即使是你。所以不会发生意外的变性。

另外,如果你的项目是多线程的,那么其它线程没法让你挂掉的。值是不变的,如果另一个线程要改变它,必须从旧的值里创建一个新值。

在 20 世纪 90 年代中期,我为生物危机( Creature Crunch )写了个游戏引擎,最大的 bug 来源就是多线程问题。我很希望能够重新了解不变性。但是我更在意是 2x 或 4x 速率 CD-ROM 驱动对游戏性能有啥区别。

不变性创建了更简单安全的代码。

这篇文章来自我的个人知乎专栏, 翻译起来感觉有些难,所以以上不一定准确。有什么地方不对的,麻烦各位大神在评论中指出来,请多多指教!

4990 次点击
所在节点    程序员
27 条回复
lfj
2016-11-18 10:14:31 +08:00
不太会用 V2EX 写文章啊,这个语法用起来感觉不太一样啊......

我的知乎专栏 “互联网科技那些事儿” https://zhuanlan.zhihu.com/intech-porter 欢迎关注
enenaaa
2016-11-18 10:20:55 +08:00
我们每篇函数式语言介绍的文章,都是神经兮兮怪话连篇, 就不能通俗易懂深入浅出地好好说话吗。
lfj
2016-11-18 10:23:45 +08:00
@enenaaa 下次努力翻译的好一些,这篇翻译有没有一些知识性问题呀?
jackisnotspirate
2016-11-18 10:26:12 +08:00
这个系列的文章写的相当好,以前看过
lfj
2016-11-18 10:31:53 +08:00
@jackisnotspirate 是不是我翻译的很烂啊 %>_<%
enenaaa
2016-11-18 10:32:39 +08:00
@lfj 抱歉, 我没仔细看完。 技术人员的时间应该都比较宝贵,我更喜欢直接了当的信息。开头的例子配图废话会挑战我的耐心,毕竟我是来看干货的, 不是来娱乐的。
zhuangzhuang1988
2016-11-18 10:38:22 +08:00
js 写函数式推荐这个 https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/
没那么多废话, 而且还带有点深度..
GavinFlying
2016-11-18 10:40:23 +08:00
原文链接在哪...?
lty494685444
2016-11-18 10:41:53 +08:00
辛普森一家;
摩登家庭;
银河护卫队;
lfj
2016-11-18 10:50:50 +08:00
@GavinFlying medium 需要翻墙,所以就没挂
lfj
2016-11-18 10:51:49 +08:00
@enenaaa 好的,懂了,之后我再简练一些
jiyinyiyong
2016-11-18 11:09:58 +08:00
学一下 Haskell, 或者 Clojure http://map.clj.im/

拿 JavaScript 学函数式编程, 就像是开着滑翔翼说要上天. 风没了你就掉下来了.
cenxun
2016-11-18 11:30:00 +08:00
赞一个~
wupher
2016-11-18 11:32:45 +08:00
最好给原文链接,至于翻墙,原来这论坛有段时间都要翻墙, V2EX 能翻墙的人肯定比你想像的人多。

原文:
https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-1-1f15e387e536#.xp1x2yoc9

markdown 二级标题应该是用 “##" 而非 "**"吧,我还以为是关键字被过滤了……

至于翻译

原文第一句

> Taking that first step to understanding Functional Programming concepts is the most important and sometimes the most difficult step. But it doesn ’ t have to be. Not with the right perspective.

你翻译成

“理解函数式编程概念是最重要,有时也是最难的一步。”
lfj
2016-11-18 11:47:27 +08:00
@wupher

翻墙问题:
不排除一部人能翻墙,但是对于不能翻墙的, medium 链接是死链。

markdown 编辑问题:
这篇文章是我用 Markdown 编辑好后直接 copy 过来的,但是预览完全是另一幅样子。 ## 显示的是 ##,但是用** 预览的时候 就是粗体,所以就改成了** 但是结果还是这样。
难道我不知道一个个改来改去,很麻烦吗?

翻译的问题:
原文我都细致看过的,至于译文,考虑到不会影响原意,所以省去了一些没必要的文字。
另外,这不是接稿翻译,所以没有必要像交稿件一样句句不漏的翻译。
不知道你觉得第一句,那么长,我省去了那些有什么不对?
murmur
2016-11-18 11:49:07 +08:00
oo+面向过程是人类的正常思维
强制函数式不用 lisp 你不感觉不太正常么
sharpy
2016-11-18 11:59:30 +08:00
想学函数式应该使用 Haskell 强调 pure 可以让你要么早早放弃要么深入根本
Technetiumer
2016-11-18 12:33:29 +08:00
第二张图好眼熟,摩登家庭?
zhuangzhuang1988
2016-11-18 13:05:33 +08:00
这个不错 <amp-youtube data-videoid="mhKl7Ppp_ao" layout="responsive" width="480" height="270"></amp-youtube>
neoblackcap
2016-11-18 13:27:56 +08:00
建议学习函数式编程的同学可以去看 Haskell 相关教程,好比文中所说的最小化副作用,那么在函数式里面应该如何做呢?这个是一个很重要的点,但是并没有提到。
而 Haskell 里面就有对应的 monad 来处理这样的情况,不去了解一下 monad 然后去用函数式编程限制副作用我认为是可怕的。副作用真的限制了吗?多步副作用依序执行会怎么样?这些都没有说,因此强烈推荐 Haskell 的相关教程。
要不真正入坑,要不尽早劝退

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/321389

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX