内容简介:在这篇由多部分组成的文章中,接下来将介绍函数式编程的一些概念,这些概念对你学习函数式编程有所帮助。如果你已经懂了什么是函数式编程,这可以加深你的理解。请不要着急。从这一点开始,花点时间阅读并理解代码示例。你甚至可能想在每节课结束后停止阅读,以便让你的观点深入理解,然后再回来完成。
在这篇由多部分组成的文章中,接下来将介绍函数式编程的一些概念,这些概念对你学习函数式编程有所帮助。如果你已经懂了什么是函数式编程,这可以加深你的理解。
请不要着急。从这一点开始,花点时间阅读并理解代码示例。你甚至可能想在每节课结束后停止阅读,以便让你的观点深入理解,然后再回来完成。
最重要的是你要理解。
纯函数(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...
编辑中可能存在的bug没法实时知道,事后为了解决这些bug,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug 。
你的点赞是我持续分享好东西的动力,欢迎点赞!
一个笨笨的码农,我的世界只能终身学习!
更多内容请关注公众号《大迁世界》!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 学会使用函数式编程的程序员(第2部分)
- 学会使用函数式编程的程序员(第3部分)
- Linux后端程序员必备技能之函数栈
- 加速函数,每个Python程序员都应该了解标准库的Lru_cache
- Java 程序员的 Kotlin 课程(二): 高阶函数与泛型的几个套路
- 程序员高薪盛宴背后:程序员正在消失?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Design Accessible Web Sites
Jeremy Sydik / Pragmatic Bookshelf / 2007-11-05 / USD 34.95
It's not a one-browser web anymore. You need to reach audiences that use cell phones, PDAs, game consoles, or other "alternative" browsers, as well as users with disabilities. Legal requirements for a......一起来看看 《Design Accessible Web Sites》 这本书的介绍吧!