你想成为一个 函数编程者么(第 1 部分)

原文出处 So You Want to be a Functional Programmer (Part 1) – Charles Scalfani – Medium

你想成为一个 函数编程者么(第 1 部分)

谈论的第一步是理解功能编程概念是最重要的,有时最困难的一步。但它不是必要的。没有正确的视野。

学习驾驶

当我们第一次学习驾驶,我们惊慌失措。当我们看到别的人驾驶时,似乎很容易。但是结果是比我们想象的难得多。

我们在父母的车上练习,真的不敢在高速公路上冒险直到我们在我们自己的临近的街道熟练了。

通过重复的练习和我们会忘记的一些恐慌的时刻,我们学会了驾驶,最终我们拿到了驾照。

驾照在手,我们可以随意的驾车出去。每一次开车,我们的车技越来越好,自信心也不断上升。我们不得不驾驶别人车的那一天会来临,或者最终我们的将会报废,我们不得不买一辆新车。

第一次站在不同车的后面是什么感觉?它是像第一次站在车轮后面?甚至还没有接近。第一次,忘记的一干二净。我们曾经坐在车内,但仅仅是一个乘客。这次我们在驾驶位上。我们控制着车。

但是当我们驾驶第二辆汽车时,我们仅仅询问了一些问题,比如车钥匙在哪,灯光在哪如何使用转向灯和如何调整后视镜。

后面,就是相当顺利的驾驶。但是相对于第一次,这一次为什么这么容易呢?

那是因为新车很像那辆旧车。一辆车所需要的基本要求是一样的,几乎都在同一个地方。

有些东西的实现是不同,也许它有一些额外的功能,但我们没有在第一次我们开车使用他们,甚至第二次。最终,我们学会所有新的功能。至少是我们关心的。

好吧,学习编程语言跟学车有很多相似之处。开头难,但是一旦处在安全区域下,后面就很容易了。

当你第一次开始学习第二门语言时,你可能会问类似的问题,“如何创建一个模块?你如何搜索数组?substring 函数的参数是什么?”

你对你可以学习一门新的语言很自信,因为它让你想起你的旧语言,也许有一些新的东西,让你的生活更容易。

你第一个宇宙飞船

不管你是一辈子开着一辆车还是几十辆车,想象一下你就要坐在宇宙飞船的后面了。

当你将要驾驶一架飞船,你不用期望你的驾驶能力在航行过程中帮助你很多。你将从零开始。(我们毕竟是程序员。需要从零计数。

你可能会带着期望开始训练,但是在太空中是非常困难的,而且驾驶这玩意儿比在地面行驶非常不同

物理没有改变。就像你在同一宇宙中航行一样。

而且这跟学习函数编程一样。你应该期望编程不一样。而且你知道的很多关于编程将_不会__翻译.

编程是思考,函数式编程将教你如何与众不同地思考。所以,你可能永远不会回到旧的思维方式。

忘记你知道的一切

人们喜欢说这个短语,但这是真的。学习函数式编程就像从头开始。不是完全的,而是有效的。有很多类似的概念,但是最好的是你必须重新开始学习

有了正确的观点,你就会有正确的期望,有了正确的期望,当事情变得困难时,你就不会放弃。

作为程序员,你已经习惯了用函数编程来做的事情。

就像在你的车里一样,你曾经备份过离开车道。但是在飞船上,没有退路。现在你可能会想,“什么,没有退路?!我怎么不能开倒车呢?”

事实上,你在宇宙飞船里不需要倒车,因为它在三维空间里有机动的能力。一旦你理解了这一点,你就再也不会错过逆转了。事实上,总有一天,你会想起汽车是如何被限制的。

学习函数式编程需要一段时间。所以耐心点。

因此,让我们退出命令式编程的冷世界,并仔细考虑函数编程的温泉。

这篇多部分文章后面的内容是函数编程概念,这些概念在进入第一个函数式语言之前会对你有所帮助。或者,如果你已经采取了行动,这将有助于提高你的理解力。

请不要急。从这一点开始阅读,并花时间去理解编码示例。你可能甚至想在每一节之后停止阅读,让思想下沉。然后返回完成。

最重要的事情是你理解

纯粹

当函数编程人员谈到纯度时,它们指的是纯函数。

纯函数是非常简单的函数。它们只对输入参数进行操作。

下面是纯函数JavaScript中的一个示例:

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

注意: add 函数没有触发变量 z。并不从变量 z 开始读代码,也不写变量 z。仅仅读它的输入 xy,然后一块返回他们求和的结果。

这是一个存函数。如果函数 add 确实访问了 z,该函数将不再是纯函数了。

下面是另外一个函数:

function justTen() {
    return 10;
}

如果函数 justTen 是一个纯函数,那么它仅仅返回一个常量。为什么?

因为我们没有给出任何输出。而且,作为纯函数,它不能访问其他只有自己的输入,返回的只有一个常量。

因为纯函数没有参数不会有效,它们不是有用的。如果 justTen 是预先定义的并作为一个常量,那将会很好。

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

考虑这个函数:

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

注意,这个函数没有任何返回。它将 xy 赋给变量 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。不是很大的惊喜,只是因为这个函数是纯函数的。

纯函数总是在给予同样的输入得到同样的输出。

因为纯函数不能改变额外的变量,所有下面的函数都不是纯函数:

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

所有这些函数都会有副作用.当你调用他们时,它们改变文件和数据库表,将数据发送至服务器或者调用 OS 来获取 Socket。他们所做的不仅仅是操作输入和输出。因此,你可以永远不会预测这些函数将返回。

纯函数没有副作用。

在指令式编程语言,比如 JavaScript,Java,和 C#,均有副作用。这使得调试非常困难,因为在你的项目中,变量可以在任何地方改变。所以当你有一个错误时,因为变量在错误的时间被改变为错误的值,你在哪里发现的,处处?这样不好。

在这点,你可能会想,“使用仅仅使用纯函数,我能做什么?!”

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

函数式编程不能根除副作用,它们仅仅能限制这些影响。由于程序必须有与现实世界接口,所以每个程序的某些部分必须是不纯的。我们的目标是最小化不纯代码的数量,并将其与程序的其他部分隔离开来。

不变性

你还记得你第一次看到下面一段代码的时候吗:

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 风格的语法,让我来解释。addOneToSum 是一个需要两个参数的函数,分别是 yz

let 块中,x 的值一定是 1,也就是说它的值在剩下的生命周期内也是 1。当函数执行完,它的生命周期就结束了,更确切地说,let的块已经求过值了。

in 作用域内,计算包含的值也定义在 let 块级作用域内,也就是,z。返回 x + y + z 的结果,更确切的说,返回1 + y + z ,因为x = 1

再一次,我听到你问“没有变量,你怎么做什么呢?”

让我们来考虑何时修改变量。这里有两个普通案例:多值修改(例如。改变一个对象或一个记录的值)和单值修改(比如,循环计数)。

函数式编程通过复制改变的值来改变记录的值。这样做可以有效地做到这一点,而不必通过使用数据结构来复制记录的所有部分。

函数式编程修改单值和复制一个值一样。

哦,是的,不是通过循环。

“没有变量,现在没有循环?我很你!!!”

等等,并不是我们不能循环,而是这里没有专门的循环结构比如 forwhiledorepeat 等等。

函数编程使用递归来循环。

在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)和一新的计数器(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

你可能会认为 foo 循环很容易理解。虽然这是有争议的,更可能的一个熟悉的问题 ,但非递归的循环需要的可变性,这是不好的。

我还没有完整地解释不可变性的优点,但在为什么编程需要限制,查看 全局可变状态 了解更多。

其中一个明显的优势是你在项目中可以访问该值,你仅仅有读的权限,也就是没有人能改变它的值。即使是你。因此没有其他的变化。

另外,如果你的项目是多线程的,那么没有其他的线程可以破坏。这个值是常量,如果其他的线程想改变它,需要从之前的值复制创建一个新值。

回到 90 年代中期,我专门针对Creature Crunch写了一个游戏引擎,bug 的最大来源是多线程问题。当时我希望我了解不可变性。但当时我更担心的是一个2倍或4倍速CD-ROM驱动器上的游戏性能之间的差异。

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

我的大脑!!!!

对于现在而言是足够的。

在本文的后续部分,我将讨论高阶函数,函数组成,柯里化等。

Up Next: Part 2

如果你喜欢,点击这?下面,其他人会在 Medium 上看到这个

如果你想加入 web 开发者学习社区,请在 ELM 函数编程使用相互帮助开发Web应用程序,请检查我的脸谱网组,学习 ELM 的编程 https://www.facebook.com/groups/learnelm/

My Twitter: @cscalfani

一拍,两拍,三拍,四十拍?

通过或多或少的掌声,你可以告诉我们哪些故事真的很突出。