迈出理解函数式编程思想的第一步是最重要的,同时也许是最难的。关于这一点,仁者见仁智者见智。
学车
当我们第一次碰车,我们痛苦并努力着,看起来很简单的操作自己上手的时候就变得比自己想象的难得多。尽管用父母的车经过多次练习,不把周围的路跑熟是不敢上高速路冒险的。但是经过不断练习和一些父母宁愿忘记的“心跳时刻”后,我们最终还是拿到了自己的驾照。
A
Ascar
翻译于 01/13 15:12
顶
其它翻译版本 (1)
有了我们的驾照, 只要有可能, 我们就会把车开出来。每次旅行, 我们都变得越来越好, 信心也增强了。后来有一天, 我们不得不开别人的车, 否则我们的车终于放弃了幽灵, 我们不得不买了一辆新的。
第一次在不同的车底开车是什么感觉?这就像第一次开车一样吗?连近在咫尺都没有。第一次, 都是那么的陌生。在那之前我们一直在车里, 但只是作为乘客。这次我们坐在驾驶座上。包含所有控件的那个。
但当我们驾驶第二辆车的时候, 我们只是问自己几个简单的问题, 比如, 钥匙去哪里了, 灯在哪里, 你怎么用转向灯, 怎么调整侧镜.
B
bokee
翻译于 01/13 20:25
顶
在那之后,航行非常顺利。 但与第一次相比,为什么这次这么容易?
那是因为这辆新车非常像旧车。 它拥有汽车所需的所有基本功能,而且它们几乎都在同一个地方。
有些东西的实现方式不同,可能还有一些额外的功能,但我们第一次开车甚至第二次都没有使用它们。 最终,我们学到了所有的新功能。 至少我们关心的是那些。这就是纯函数。如果add函数真的访问了z,它就不再是纯函数了。
这是另一个需要考虑的函数:
function justTen() {
return 10;
}
如果函数, justTen, 是纯的,那么它只能返回一个常量。为什么呢?
因为我们没有给它任何输入。而且,既然是纯的,它不能访问除了它自己的输入以外的任何参数,它唯一可以返回的值是常量。
由于不带参数的纯函数不做任何事情,因此它们不是很有用。如果justTen被定义为常量也需会更好。
这就是纯函数。如果add函数真的访问了z,它就不再是纯函数了。
这是另一个需要考虑的函数:
function justTen() {
return 10;
}
如果函数, justTen, 是纯的,那么它只能返回一个常量。为什么呢?
因为我们没有给它任何输入。而且,既然是纯的,它不能访问除了它自己的输入以外的任何参数,它唯一可以返回的值是常量。
由于不带参数的纯函数不做任何事情,因此它们不是很有用。如果justTen被定义为常量也需会更好。
多数有用的纯函数需要至少有一个参数。
考虑下下面函数:
function addNoReturn(x, y) {
var z = x + y
}
注意这个函数如何实现不返回任意值。它将x和y相加,然后将其放到变量z中,但并不返回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);
所有这些功能都有所谓的副作用。 当你调用它们时,它们会更改文件和数据库表,将数据发送到服务器或调用操作系统以获取套接字。 他们所做的不仅仅是操作他们的输入和返回输出。 因此, 你永远无法预测这些函数将返回什么。
纯功能没有副作用。
在诸如Javascript,Java和C#等命令式编程语言中,副作用无处不在。这使调试非常困难,因为变量可以在程序中的任务位置更改。所以当你遇到一个在错误时间由于变量更改为错误值所引起的问题时,你去哪里查找?到处?这不好的。
在这一点上,您可能会想,“我怎么可能只使用纯函数来做任何事情?”
在函数式编程中,您不只是编写纯函数。
函数式语言不能消除副作用,只能限制它们。由于程序必须与现实世界相连接,所以每个程序的某些部分必须是不纯的。函数式语言的目标是最小化不纯代码的数量,并将其与程序的其它部分隔离开来。
不变性(Immutability)
你还记得当你第一次看到下面这段代码时:
var x = 1;
x = x + 1;
不管是谁在教你,他都会要求你忘掉在数学课上学到的东西。在数学里,x 永远不可能等于 x + 1。
但在命令式编程中,它的意思是,取 x 的当前值,加上“1”,然后把结果放回x 中。
好吧,在函数式编程中,x = x + 1 是非法的。所以你必须“记住”你在数学上忘记的东西……在某种程度上,
函数式编程中没有变量。
由于历史原因,已存储的值仍然被称为变量,但它们实际上是常量,也就是说,一旦 x 取了一个值,那这个值就是 x 整个程序生命周期中的确定的值。
别担心,x 通常是一个局部变量,所以它的生命周期通常很短。但只要它活着,就永远不会改变。
下面是 Elm 中的常量变量示例,Elm 是一种用于 Web 开发的纯函数式编程语言:
addOneToSum y z =
let
x = 1
in
x + y + z
如果您不熟悉 ML 风格的语法,让我解释一下。“addOneToSum”是一个包含两个参数, y 和 z 的函数。
在 let 代码块中,x 被绑定到 1 的值上,也就是说,在接下来的生命周期中它都等于 1。当函数退出时,或者更准确地说,在计算 let 代码块时,它的生命周期就结束了。
在 in 代码块中,计算将会包括在“let”块中的值,即 x。手机号买卖平台返回计算结果为 x + y + z,或者更准确地说,返回 1 + y + z,因为 x = 1。
再一次,我还是会听到你问:“我怎么能在没有变量的情况下做任何事情?!”
当想要修改变量的时候让我们来思考一下。可以想到两种一般情况:多值变化(例如,改变对象或记录的单个值)和单值变化(例如,循环计数器)。
函数式编程是通过创建一个已更改值的记录副本来处理记录中值的更改。它可以有效地完成这项工作,而无需使用可实现此目的的数据结构来复制记录的所有部分。
函数式编程通过复制它这种完全相同的方式来解决单值更改。
哦,是的,没有循环。
“什么,没有变量,现在没有循环?! 我恨你!!!”
稍等。这并不是我们不能做循环(没有双关语), 只是没有特定的循环结构,如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
注意,函数方法如何通过使用新的start(start + 1)和_new_accumulator(acc + start)调用自身来完成与for循环相同的递归操作。 它不会修改旧值。 相反,它使用通过旧值计算出的新值。
不幸的是,即使您花了一点时间研究它,也很难在Javascript中看到,原因有两个。 一,Javascript的语法很吵,二,你可能不习惯递归思考。
使用Elm语言,更容易阅读,因此,理解:
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循环更容易理解。 虽然这是值得商榷的,更可能是熟悉度的问题,但非递归循环需要可变性,这很糟糕。
我还没有完全解释不变性在这里的好处,但请查看为什么程序员需要限制的全局可变状态部分以了解更多信息。
一个明显的好处是,如果你可以访问程序中的值,则只具有读取权限,这意味着没有其他人可以更改该值,即便是你。 所以没有偶然的突变。
此外,如果您的程序是多线程的,那么没有其他线程可以给你制造麻烦。 该值是常量,如果另一个线程想要更改它,它将通过旧值创建一个新值。
早在90年代中期,我就为生物紧缩编写了一个游戏引擎,最大的错误根源是多线程问题。 我希望我当时知道不变性。 但那时我更担心2倍速或4倍速CD-ROM驱动器在游戏性能上的差异。
不变性可以创建更简单,更安全的代码。
我的脑子!!!
够了。
在本文的后续部分中,我将讨论高阶函数,函数组合,柯里化等。