学会使用函数式编程的程序员(第1部分)

43次阅读

共计 6093 个字符,预计需要花费 16 分钟才能阅读完成。

在这篇由多部分组成的文章中,接下来将介绍函数式编程的一些概念,这些概念对你学习函数式编程有所帮助。如果你已经懂了什么是函数式编程,这可以加深你的理解。
请不要着急。从这一点开始,花点时间阅读并理解代码示例。你甚至可能想在每节课结束后停止阅读,以便让你的观点深入理解,然后再回来完成。
最重要的是你要理解。
纯函数(Purity)

所谓纯函数,就是指这样一个函数,对于相同的输入,永远得到相同的输出,它不依赖外部环境,也不会改变外部环境。如果不满足以上几个条件那就是非纯函数。
下面是 Javascript 中的一个纯函数示例:
var z = 10;
function add(x, y) {
return x + y;
}

注意,add 函数不涉及 z 变量。它不从 z 读取,也不从 z 写入,它只读取 x 和 y,然后返回它们相加的结果。这是一个纯函数。如果 add 函数确实访问了变量 z,那么它就不再是纯函数了。
请思考一下下面这个函数:
function justTen() {
return 10;
}

如果函数 justTen 是纯的,那么它只能返回一个常量, 为什么?
因为我们没有给它任何参数。而且,既然是纯函数的,除了自己的输入之外它不能访问任何东西,它唯一可以返回的就是常量。
由于不带参数的纯函数不起作用,所以它们不是很有用。所以 justTen 被定义为一个常数会更好。
大多数有用的纯函数必须至少带一个参数。
考虑一下这个函数:
function addNoReturn(x, y) {
var z = x + y
}

注意这个函数是不返回任何值。它只是把变量 x 和 y 相加赋给变量 z,但并没有返回。
这个也是一个纯函数,因为它只处理输入。它确实对输入的变量进行操作,但是由于它不返回结果,所以它是无用的。
所有有用的纯函数都必须返回一些我们期望的结果。
让我们再次考虑第一个 add 函数:

注意 add(1, 2) 的返回结果总是 3。这不是奇怪的事情,只是因为 add 函数是纯的。如果 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 中执行循环:

注意,递归是一种函数式方法,它通过使用一个结束条件 start (start + 1) 和调用自己 accumulator (acc + start) 来实现与 for 循环相同的功能。它不会修改旧的值。相反,它使用从旧值计算的新值。
不幸的是,这在 Javascript 中 很难想懂,需要你花点时间研究它,原因有二。第一,Javascript 的语法相对其它高级语言比较乱,其次,你可能还不习惯递归思维。
在 Elm,它更容易阅读,如下:
sumRange start end acc =
if start > end then
acc
else
sumRange (start + 1) end (acc + start)

它是这样运行的:

你可能认为 for 循环更容易理解。虽然这是有争议的,而且更可能是一个熟悉的问题,但非递归循环需要可变性,这是不好的。
在这里,我还没有完全解释不变性的好处,但是请查看全局可变状态部分,即为什么程序员需要限制来了解更多。
我还没有完全解释不可变性 (Immutability) 在这里的好处,但请查看 为什么程序员需要限制的全局可变状态部分 以了解更多信息。
不可变性的好处是,你读取访问程序中的某个值,但只有读权限的,这意味着不用害怕其他人更改该值使自己读取到的值是错误。
不可变性的还有一个好处是,如果你的程序是多线程的,那么就没有其他线程可以更改你线程中的值,因为该值是不可变,所以另一个线程想要更改它,它只能从旧线程创建一个新值。
不变性可以创建更简单、更安全的代码。
重构
让我们考虑一下重构,下面是一些 Javascript 代码:

我们以前可能都写过这样的代码,随着时间的推移,开始意识到这两个函数实际上是相同的,函数名称,打印结果不太一样而已。
我们不应该复制 validateSsn 来创建 validatePhone,而是应该创建一个函数(共同的部分),通过参数形式实现我们想要的结果。
重构后的代码如下:

旧代码参数中 ssn 和 phone 现在用 value 表示,正则表达式 /^\d{3}-\d{2}-\d{4}$/ and /^\(\d{3}\)\d{3}-\d{4}$/ 由变量 regex. 表示。最后,消息“SSN”和“电话号码”由变量 type 表示。
这个有类似的函数都可以使用这个函数来实现,这样可以保持代码的整洁和可维护性。
高阶函数

许多语言不支持将函数作为参数传递, 有些会支持但并不容易。
在函数式编程中,函数是一级公民。换句话说,函数通常是另一个函数的值。
由于函数只是值,我们可以将它们作为参数传递。即使 Javascript 不是纯函数语言,也可以使用它进行一些功能性的操作。所以这里将上面的两个函数重构为单个函数,方法是将验证合法性的函数作为函数 parseFunc 的参数:
function validateValueWithFunc(value, parseFunc, type) {
if (parseFunc(value))
console.log(‘Invalid ‘ + type);
else
console.log(‘Valid ‘ + type);
}

像函数 parseFunc 接收一个或多个函数作为输入的函数,称为 高阶函数。
高阶函数要么接受函数作为参数,要么返回函数,要么两者兼而有之。
现在可以调用高阶函数(这在 Javascript 中有效,因为 Regex.exec 在找到匹配时返回一个 truthy 值):
validateValueWithFunc(‘123-45-6789’, /^\d{3}-\d{2}-\d{4}$/.exec, ‘SSN’);
validateValueWithFunc(‘(123)456-7890’, /^\(\d{3}\)\d{3}-\d{4}$/.exec, ‘Phone’);
validateValueWithFunc(‘123 Main St.’, parseAddress, ‘Address’);
validateValueWithFunc(‘Joe Mama’, parseName, ‘Name’);

这比有四个几乎相同的函数要好得多。
但是请注意正则表达式,这里有点冗长了。简化一下:
var parseSsn = /^\d{3}-\d{2}-\d{4}$/.exec;
var parsePhone = /^\(\d{3}\)\d{3}-\d{4}$/.exec;
validateValueWithFunc(‘123-45-6789’, parseSsn, ‘SSN’);
validateValueWithFunc(‘(123)456-7890’, parsePhone, ‘Phone’);
validateValueWithFunc(‘123 Main St.’, parseAddress, ‘Address’);
validateValueWithFunc(‘Joe Mama’, parseName, ‘Name’);

现在看起来好多了。现在,当要验证一个电话号码时,不需要复制和粘贴正则表达式了。
但是假设我们有更多的正则表达式需要解析,而不仅仅是 parseSsn 和 parsePhone。每次创建正则表达式解析器时,我们都必须记住在末尾添加 .exec,这很容易被忘记。
可以通过创建一个返回 exec 的高阶函数来防止这种情况:
function makeRegexParser(regex) {
return regex.exec;
}
var parseSsn = makeRegexParser(/^\d{3}-\d{2}-\d{4}$/);
var parsePhone = makeRegexParser(/^\(\d{3}\)\d{3}-\d{4}$/);
validateValueWithFunc(‘123-45-6789’, parseSsn, ‘SSN’);
validateValueWithFunc(‘(123)456-7890’, parsePhone, ‘Phone’);
validateValueWithFunc(‘123 Main St.’, parseAddress, ‘Address’);
validateValueWithFunc(‘Joe Mama’, parseName, ‘Name’);

这里,makeRegexParser 采用正则表达式并返回 exec 函数,该函数接受一个字符串。validateValueWithFunc 将字符串 value 传递给 parse 函数,即 exec。
parseSsn 和 parsePhone 实际上与以前一样,是正则表达式的 exec 函数。
当然,这是一个微小的改进,但是这里给出了一个返回函数的高阶函数示例。但是,如果 makeRegexParser 要复杂得多,这种更改的好处是很大的。
下面是另一个返回函数的高阶函数示例:
function makeAdder(constantValue) {
return function adder(value) {
return constantValue + value;
};
}

函数 makeAdder,接受参数 constantValue 并返回函数 adder,这个函数返回 constantValue 与它传入参数相加结果。
下面是它的用法:
var add10 = makeAdder(10);
console.log(add10(20)); // 打印 30
console.log(add10(30)); // 打印 40
console.log(add10(40)); // 打印 50

我们通过将常量 10 传递给 makeAdder 来创建一个函数 add10, makeAdder 返回一个函数,该函数将向返回的结果都加 10。
注意,即使在 makeAddr 返回之后,函数 adder 也可以访问变量 constantValue。这里能访问到 constantValue 是因为存在闭包。
闭包机制非常重要,因为如果没有它,返回函数的函数就不会有很大作用。所以必须了解它们是如何工作。
闭包

下面是一个使用闭包的函数的示例:
function grandParent(g1, g2) {
var g3 = 3;
return function parent(p1, p2) {
var p3 = 33;
return function child(c1, c2) {
var c3 = 333;
return g1 + g2 + g3 + p1 + p2 + p3 + c1 + c2 + c3;
};
};
}

在这个例子中,child 函数可以访问它自身的变量,函数 parent 函数可以访问它的自身变量和函数 grandParent 的变量。而函数 grandParent 只能访问自身的变量。
下面是它的一个使用例子:
var parentFunc = grandParent(1, 2); // returns parent()
var childFunc = parentFunc(11, 22); // returns child()
console.log(childFunc(111, 222)); // prints 738
// 1 + 2 + 3 + 11 + 22 + 33 + 111 + 222 + 333 == 738

在这里,parentFunc 保留了 parent 的作用域,因为 grandParent 返回 parent。
类似地,childFunc 保留了 child 的作用域,因为 parentFunc 保留了 parent 的作用域,而 parent 的作用域 保留了 child 的作用域。
当一个函数被创建时,它在创建时作用域中的所有变量在函数的生命周期内都是可访问的。一个函数只要还有对它的引用就存在。例如,只要 childFunc 还引用 child 的作用域,child 的作用域就存在。
闭包具体还看看之前整理的一篇文章:我从来不理解 JavaScript 闭包,直到有人这样向我解释它 …
原文:1、https://medium.com/@cscalfani…2、https://medium.com/@cscalfani…
你的点赞是我持续分享好东西的动力,欢迎点赞!
一个笨笨的码农,我的世界只能终身学习!
更多内容请关注公众号《大迁世界》!

正文完
 0

学会使用函数式编程的程序员(第1部分)

43次阅读

共计 4170 个字符,预计需要花费 11 分钟才能阅读完成。

学习函数式编程的第一步也是最重要的一步就是知道它是什么,做什么用的。
学开车

我们刚开始学开车时,很吃力。当我们看到别人在做这件事的时候,看起来确实很容易。但结果比我们想象的要难。我们在父母的车里练习,直到熟悉了附近的街道,我们才敢在公路上冒险。但是经过反复的练习,我们学会了开车,最终拿到了驾照。
有了执照,我们一有机会就把车开出去。每次短暂旅行,我们驾车技术变得越来越好,随之我们的信心逐渐上升。或许有那么一天我们不得不开别人的车或者我们的老车就报废了不得不买一辆新的时候。
第一次驾驶一辆不同的车是什么感觉? 就像第一次开车一样吗? 第一次,一切都那么陌生。我们之前坐过车,但只是作为乘客。这一次我们掌握了主动权可以自己开车。
但是当我们开第二辆车的时候,我们只是自问几个简单的问题,比如,插钥匙口去在哪个位置,灯在哪里,怎么使用转弯信号,怎么调整侧视镜等等。
从那以后,一切都很顺利。但是为什么这次和第一次相比如此容易呢?
那是因为新车和旧车很像。它拥有汽车所需要的所有基本的东西,而且它们几乎在同一个地方。
新车与旧车有些东西的构造不同,可能还有一些额外的功能,但我们第一次开车甚至第二次都没有使用它们。但是,最终我们还是学会我们关心的那些内容。
所以学习编程语言也是类似学开车一样。第一次是最难的,但一旦你有了第一次经验,后续的就会更容易。
当你刚开始学习第二门语言时,你会问这样的问题,“我如何创建一个模块? 如何搜索数组? substring 函数的参数是什么?“
你相信你能学会驾驶这门新语言,因为它会让你想起你的旧语言,也许有一些新东西可以让你的生活更轻松。
如果你要驾驶宇宙飞船,你不会期望你在路上的驾驶能力会对你有很大的帮助。
你的第一个宇宙飞船

不管你一辈子开的是一辆车还是几十辆车,想象一下你即将驾驶一艘宇宙飞船。在开始训练的时候,你会预想太空中的场景与地面不现,宇宙飞船架使方式与汽车的驾驶方式也是不同的。但是物理世界没有改变,就像你在同一个宇宙中遨游一样。
这和学习函数式编程是一样的。你应该预想到代码风格与你之前所编写不太一样,思路也是有很大差异的。
非函数式编程与函数式编程的思维模式大相径庭。当你学会了,你会喜欢上它以至于你可能永远不会回到原有旧思维模式。
忘记你所知道的一切

人们喜欢说这句话,但这有点真实。学习函数式编程就像从头开始。有很多相似的概念,但如果你只是希望你必须重新学习所有东西,那就最好了。当你有了想要学习函数式编程的欲望,有会有学习动力,有了动力,当事情变得困难时,你就不会放弃。
作为一名程序员,有很多事情是你习惯做的,但是你不能再用函数式编程来做了。就像在你的车里一样,你过去常常倒车离开车道。但是在宇宙飞船里,没有倒车。现在你可能会想,“什么? 没有反向? 我怎么能不倒车呢?”
事实证明,在宇宙飞船里你不需要倒车因为它在三维空间里的推动能力。一旦你明白了这一点,你就再也不会想要倒车了。事实上,有一天,你会回想起汽车的局限性。
学习函数式编程需要一段时间,所以要有耐心。
因此,让我们离开命令式编程这个冰冷的世界,来温习一下函数式编程的温泉。
在这篇由多部分组成的文章中,接下来将介绍函数式编程的一些概念,这些概念对你学习函数式编程有所帮助。如果你已经懂了什么是函数式编程,这可以加深你的理解。
请不要着急。从这一点开始,花点时间阅读并理解代码示例。你甚至可能想在每节课结束后停止阅读,以便让你的观点深入理解,然后再回来完成。
最重要的是你要理解。
纯函数(Purity)

所谓纯函数,就是指这样一个函数,对于相同的输入,永远得到相同的输出,它不依赖外部环境,也不会改变外部环境。如果不满足以上几个条件那就是非纯函数。
下面是 Javascript 中的一个纯函数示例:
var z = 10;
function add(x, y) {
return x + y;
}

注意,add 函数不涉及 z 变量。它不从 z 读取,也不从 z 写入,它只读取 x 和 y,然后返回它们相加的结果。这是一个纯函数。如果 add 函数确实访问了变量 z,那么它就不再是纯函数了。
请思考一下下面这个函数:
function justTen() {
return 10;
}

如果函数 justTen 是纯的,那么它只能返回一个常量, 为什么?
因为我们没有给它任何参数。而且,既然是纯函数的,除了自己的输入之外它不能访问任何东西,它唯一可以返回的就是常量。
由于不带参数的纯函数不起作用,所以它们不是很有用。所以 justTen 被定义为一个常数会更好。
大多数有用的纯函数必须至少带一个参数。
考虑一下这个函数:
function addNoReturn(x, y) {
var z = x + y
}

注意这个函数是不返回任何值。它只是把变量 x 和 y 相加赋给变量 z,但并没有返回。
这个也是一个纯函数,因为它只处理输入。它确实对输入的变量进行操作,但是由于它不返回结果,所以它是无用的。
所有有用的纯函数都必须返回一些我们期望的结果。
让我们再次考虑第一个 add 函数:

注意 add(1, 2) 的返回结果总是 3。这不是奇怪的事情,只是因为 add 函数是纯的。如果 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 中执行循环:

注意,递归是一种函数式方法,它通过使用一个结束条件 start (start + 1) 和调用自己 accumulator (acc + start) 来实现与 for 循环相同的功能。它不会修改旧的值。相反,它使用从旧值计算的新值。
不幸的是,这在 Javascript 中 很难想懂,需要你花点时间研究它,原因有二。第一,Javascript 的语法相对其它高级语言比较乱,其次,你可能还不习惯递归思维。
在 Elm,它更容易阅读,如下:
sumRange start end acc =
if start > end then
acc
else
sumRange (start + 1) end (acc + start)

它是这样运行的:

你可能认为 for 循环更容易理解。虽然这是有争议的,而且更可能是一个熟悉的问题,但非递归循环需要可变性,这是不好的。
在这里,我还没有完全解释不变性的好处,但是请查看全局可变状态部分,即为什么程序员需要限制来了解更多。
我还没有完全解释不可变性 (Immutability) 在这里的好处,但请查看 为什么程序员需要限制的全局可变状态部分 以了解更多信息。
不可变性的好处是,你读取访问程序中的某个值,但只有读权限的,这意味着不用害怕其他人更改该值使自己读取到的值是错误。
不可变性的还有一个好处是,如果你的程序是多线程的,那么就没有其他线程可以更改你线程中的值,因为该值是不可变,所以另一个线程想要更改它,它只能从旧线程创建一个新值。
不变性可以创建更简单、更安全的代码。
在本文的后续部分中,将讨论高阶函数、函数组合、局部套用等等,尽请期待!
原文:https://medium.com/@cscalfani…
编辑中可能存在的 bug 没法实时知道,事后为了解决这些 bug, 花了大量的时间进行 log 调试,这边顺便给大家推荐一个好用的 BUG 监控工具 Fundebug。
你的点赞是我持续分享好东西的动力,欢迎点赞!
一个笨笨的码农,我的世界只能终身学习!
更多内容请关注公众号《大迁世界》!

正文完
 0