如果你想做一个函数编程程序员(第一部分)
原文链接:https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-1-1f15e387e536
这是一篇自译的文章(来源于机译和自己的理解),希望对大家有所启发。(如有侵权,请与我联系,侵权必删!!!)
迈出第一步来理解函数式编程概念是最重要的,有时也是最困难的一步。关于这一点,仁者见仁智者见智。
学开车
当我们第一次学习驾驶时,我们就挣扎了。当我们看到其他人这样做时,看起来确实很容易。但是事实证明,这比我们想象的要难。
我们在父母的车上练习,直到我们熟练的在自己社区的街道行驶,我们才真正走上高速公路。
但是通过反复的练习和一些父母想忘记的惊慌时刻,我们学会了开车,并最终获得了驾照。
当我们有了执照,我们将尽可能地将汽车发挥到极致。每次旅行,我们都变得越来越好,我们的信心也增强了。到了第二天,我们不得不驾驶别人的汽车,或者我们的汽车终于抛锚,我们不得不买了一辆新的。
第一次开另一辆车是什么感觉?就像是第一次开车一样吗?差得远了。第一次,一切都如此陌生。在那之前我们去过汽车,但只是作为乘客。这次我们坐在驾驶员座位上。具有所有控件的控制权。
但是,当我们开第二辆车时,我们只是问自己几个简单的问题,例如钥匙在哪里,灯在哪里,如何使用转向信号灯以及如何调整后视镜。
之后,航行非常顺畅。但是,为什么这次与第一次相比如此轻松?
那是因为新车和旧车很像。它具有汽车所需的所有基本要素,并且它们几乎位于同一位置。
一些事情的实现方式有所不同,也许还有一些其他功能,但是我们第一次开车甚至第二次就没有再使用它们。最终,我们了解了所有新功能。至少我们关心的功能。
好吧,学习编程语言就是这样。首先是最难的。但是一旦您拥有一个安全带,随后的操作就容易了。
刚开始使用第二种语言时,您会问诸如“如何创建模块?如何搜索数组?字符串函数的参数是什么?”
您有信心可以学习使用这种新语言,因为它可能会通过一些新的东西使您想起您的旧语言,希望可以使您的学习更轻松。
您的第一艘太空飞船
无论您是一生驾驶一辆汽车还是数十辆汽车,都无法成为您将成为飞船驾驶者的后盾。
如果您要驾驶一艘太空飞船,您不会指望您在路上的驾驶能力对您有很大帮助。您将从零开始。(毕竟我们是程序员。我们从零开始计数。)
开始训练时,您会期望太空中的事物完全不同,而驾驶这种设备与在地面上行驶完全不同。
物理没有改变。就像您在同一个宇宙中导航的方式一样。
学习函数式编程也是如此。您应该期望事情会大不相同。而且您对编程了解的大部分内容都不会转变。
编程是在思考,而函数式编程将教您以不同的方式思考。如此之多,以至于您可能永远也不会回到旧的思维方式。
忘记你所知道的一切
人们喜欢说这句话,但这是真的。学习函数式编程就像从头开始。不完全,但是有效。有很多类似的概念,但是最好希望您重新学习所有内容。
有了正确的观点,您将拥有正确的期望,有了正确的期望,当事情变得困难时,您将不会放弃。
作为程序员,您已经习惯了各种各样的事情,而使用函数式编程则再也做不到。
就像在车上一样,您过去经常掉头以离开车道。但是在太空飞船中,没有反向。现在您可能会想,“什么?不可逆转?!我应该如何驾驶而不要倒车?!”
好吧,事实证明,由于它具有在三维空间中操纵的能力,因此您不需要在太空飞船中反向飞行。一旦了解了这一点,您将永远不会再错过反向。实际上,总有一天,您会回想起汽车的实际限制。
学习函数式编程需要一段时间。所以要耐心点。
因此,让我们退出命令式编程的冷酷世界,并温和地浸入函数式编程的温泉中。
在此部分的文章中,后面介绍的是函数式编程概念,它们将在您深入学习第一种函数式语言之前为您提供帮助。或者,如果您已经尝试过了,这将有助于您进一步理解。
请不要着急。从这一点开始,花点时间阅读,并花时间了解编码示例。您甚至可能希望在每个部分结束后停止阅读,以使想法沉入其中。然后稍后返回以完成操作。
最重要的是你要了解。
Purity
当功能程序员谈论Purity时,他们指的是纯函数(Pure Functions)。
纯函数是非常简单的函数。它们仅对输入参数进行操作。
这是纯函数的Javascript示例:
var z = 10;
function add(x, y) {
return x + y;
}
请注意,add函数不会触及z变量。它不会从z读取,也不会写入z。它仅读取x和y的输入,并返回将它们加在一起的结果。
这是一个纯粹的函数。如果add函数确实访问了z,那么它将不再是纯函数。
这是要考虑的另一个功能:
function justTen() {
return 10;
}
如果函数justTen是纯函数,则它只能返回一个常量。为什么?
因为我们没有给它任何输入。而且,从Purity的角度来看,除了它自己的输入之外,它无法访问其他任何东西,因此它唯一可以返回的就是常量。
由于不带参数的纯函数不起作用,因此它们不是很有用。如果将justTen定义为常量会更好。
最有用的纯函数必须至少使用一个参数。
考虑以下功能:
function addNoReturn(x, y) {
var z = x + y
}
注意此函数不返回任何内容。它将x和y相加并将其放入变量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来获取socket。他们所做的不只是对输入和返回输出进行操作。因此,您永远无法预测这些函数将返回什么。
纯函数没有副作用。
在命令式编程语言(例如Javascript,Java和C#)中,副作用无处不在。这使调试非常困难,因为可以在程序中的任何位置更改变量。那么,当您因为在错误的时间将变量更改为错误的值而遇到错误时,您会在哪里看呢?Everywhere?这不好。
在这一点上,您可能会想:“我该怎么做,仅使用纯函数?!”
在函数式编程中,您不仅可以编写纯函数。
功能语言不能消除副作用,只能限制它们。由于程序必须与真实世界交互,因此每个程序的某些部分必须不纯。目的是最大程度地减少不纯代码的数量,并将其与我们程序的其余部分隔离。
不变性
您还记得第一次看到以下代码的时候:
var x = 1;
x = x + 1;
谁在告诉你,让你忘记在数学课上学到的东西?在数学上,x永远不能等于x + 1。
但是在命令式编程中,这意味着将x的当前值加1并将其结果放回 x中。
好吧,在函数式编程中,x = x + 1是非法的。因此,您必须记住您在数学上忘记的事情……。
在函数式编程中没有变量。
由于历史的原因,存储的值仍被称为变量,但它们是常量,即,一旦x取一个值,它就是生命周期中的那个值。
不用担心,x通常是局部变量,因此其寿命通常很短。但是,即使它还活着,它也永远不会改变。
这是Elm(一种用于Web开发的纯函数编程语言)中的常量变量的示例:
addOneToSum y z =
let
x = 1
in
x + y + z
如果您不熟悉ML-Style语法,请让我解释一下。addOneToSum是一个带有y和z两个参数的函数。
在let块内部,x绑定为1的值,即在其余下的时间里等于1。当函数退出时,它的寿命结束了;或者在执行let块时,它的寿命结束了。
在in块内部,计算可以包括在let块中定义的值x,返回x + y + z的计算结果或更准确地说,由于x = 1,返回1 + y + z。
再一次,我听到你问“我应该如何做无变量的任何事情?!”
让我们考虑一下何时要修改变量。有两种一般情况:多值更改(例如,更改对象或记录的单个值)和单值更改(例如,循环计数器)。
函数式编程通过复制值已更改的记录来处理记录中的值更改。它可以有效地做到这一点,而不必使用新的数据结构来复制记录的所有部分。
函数式编程通过复制旧值,来解决单值更改。
哦,是的,没有循环。
“没有变量,没有循环吗?!我恨你!!!”
坚持,稍等。并不是说我们不能做循环(没有双关语),只是没有特定的循环结构,如for,while,do,repeat等。
函数式编程使用递归进行循环。
您可以通过以下两种方式在Javascript中执行循环:
//简单的循环结构
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)和新的累加器(acc + start)调用自身来实现与for循环相同的功能。它不会修改旧值。取而代之的是,它使用从旧计算得出的新值。
不幸的是,即使您花一点时间研究Java语言,也很难在Java中看到这一点,这有两个原因。第一,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循环更容易理解。尽管这值得商榷,而且很可能是熟悉性的问题,但非递归循环却需要Mutability,这很糟糕。
在这里,我没有完全解释不可变性的好处,但请查阅“程序员为什么需要限制”中的“全局可变状态”部分以了解更多信息。
一个明显的好处是,如果您有权访问程序中的一个值,则您只有读访问权限,这意味着没有其他人可以更改该值。甚至是你,因此没有意外的突变。
同样,如果您的程序是多线程的,那么没有其他线程可以将程序中的一个值更改。该值是常数,如果另一个线程想要更改它,它将从旧的值创建一个新的值。
早在90年代中期,我就为Creature Crunch编写了一个游戏引擎,其中最大的bug来源是多线程问题。我希望我那时能了解不变性。但是那时候我更担心2倍或4倍速CD-ROM驱动器在游戏性能上的差异。
不变性创建了更简单,更安全的代码。
我的脑子!!!!
现在足够了。
在本文的后续部分中,我将讨论高阶函数,函数组成,Currying等。
接下来:第二部分