如果你想做一个函数编程程序员(第二部分)
原文链接:https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-2-7005682cec4a
这是一篇自译的文章(来源于机译和自己的理解),希望对大家有所启发。(如有侵权,请与我联系,侵权必删!!!)
迈出第一步来理解函数式编程概念是最重要的,有时也是最困难的一步。关于这一点,仁者见仁智者见智。
先前的部分:第1部分
温馨的提示
请慢慢阅读代码。在继续之前,请确保您了解它。每个部分都建立在上一部分的基础上。
如果着急,您可能会错过一些细节,这将在以后变得很重要。
重构
让我们考虑一下重构。这是一些Javascript代码:
function validateSsn(ssn) {
if (/^\d{3}-\d{2}-\d{4}$/.exec(ssn))
console.log('Valid SSN');
else
console.log('Invalid SSN');
}
function validatePhone(phone) {
if (/^\(\d{3}\)\d{3}-\d{4}$/.exec(phone))
console.log('Valid Phone Number');
else
console.log('Invalid Phone Number');
}
我们都编写过类似的函数随着时间的推移,我们开始认识到,这两个功能都几乎相同,只有几件事情。
代替复制validateSsn并粘贴和编辑以创建validatePhone,我们应该创建一个函数并参数化粘贴后编辑的内容。
在此示例中,我们将参数化value,正则表达式和打印的消息(至少打印消息的最后一部分)。
重构后的代码:
function validateValue(value, regex, type) {
if (regex.exec(value))
console.log('Invalid ' + type);
else
console.log('Valid ' + type);
}
现在,旧代码中的参数ssn和phone由value表示。
正则表达式/ ^ \ d {3}-\ d {2}-\ d {4} $ /和/ ^ \(\ d {3} \)\ d {3}-\ d {4} $ /是以regex表示。
最后,消息“ SSN”和“电话号码”的最后部分由type表示。
具有一个功能比具有两个功能要好得多。或更糟的是三个,四个或十个功能。这样可以使您的代码保持干净和可维护。
例如,如果存在错误,则只需要将其修复在一个位置即可,而不是在整个代码库中进行搜索以找到可以粘贴和修改此功能的位置。
但是,当您遇到以下情况时会发生什么:
function validateAddress(address) {
if (parseAddress(address))
console.log('Valid Address');
else
console.log('Invalid Address');
}
function validateName(name) {
if (parseFullName(name))
console.log('Valid Name');
else
console.log('Invalid Name');
}
这里的parseAddress和parseFullName是采用字符串并在解析后返回true的函数。
我们如何重构呢?
好了,我们可以像以前一样使用value作为地址和名称,并为’Address’和’Name’输入类型,但是有一个函数使用了我们的正则表达式。
如果我们可以将函数作为参数传递……
高阶函数
许多语言不支持将传递函数用作参数。有些可以,但是却不容易。
在函数式编程中,函数是语言的一等公民。换句话说,函数只是一个值。
由于函数只是值,我们可以将它们作为参数传递。
即使Javascript不是纯功能语言,您也可以使用它进行一些功能性操作。因此,这是通过将解析函数作为名为parseFunc的参数传递而重构为单个函数的最后两个函数:
function validateValueWithFunc(value, parseFunc, type) {
if (parseFunc(value))
console.log('Invalid ' + type);
else
console.log('Valid ' + type);
}
我们的新函数称为高阶函数。
高阶函数要么将函数用作参数,要么将函数返回。
现在我们可以为前面的四个函数调用高阶函数(这在Javascript中有效,因为在找到匹配项时Regex.exec返回真实值):
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传递给解析函数,即exec。
parseSsn和parsePhone实际上与之前的正则表达式的exec函数相同。
诚然,这是一个微不足道的改进,但此处显示的是给出返回函数的高阶函数的示例。
但是,您可以想象如果makeRegexParser复杂得多的话,进行此更改的好处。
这是返回函数的高阶函数的另一个示例:
function makeAdder(constantValue) {
return function adder(value) {
return constantValue + value;
};
}
在这里,我们有makeAdder,它接受constantValue并返回adder,该函数会将该常量添加到它传递的任何值中。
使用方法如下:
var add10 = makeAdder(10);
console.log(add10(20)); // prints 30
console.log(add10(30)); // prints 40
console.log(add10(40)); // prints 50
我们通过将常量10传递给makeAdder来创建一个函数add10,该函数返回一个将为所有内容加10的函数。
请注意,即使在makeAddr返回之后,函数加法器也可以访问constantValue。这是因为在创建加法器时constantValue在其范围内。
此行为非常重要,因为如果没有它,返回函数的函数将不会很有用。因此,重要的是我们了解它们的工作方式以及这种行为的含义。
这种行为称为Closure。
闭包
这是使用闭包的人为设计的示例:
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;
};
};
}
在此示例中,子级可以访问其变量,父级变量和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
在这里,由于grandParent返回parent,所以parentFunc使父范围保持活动状态。
同样,由于仅是父级的parentFunc返回child,childFunc可以使子级的作用域保持活动状态。
创建函数时,在函数的整个生命周期中,它在创建时在其作用域内的所有变量都可以访问。只要存在对函数的引用,该函数就存在。例如,只要childFunc仍引用子级的范围,它就存在。
闭包是函数的作用域,通过引用该函数可以使该作用域保持活动状态。
请注意,在Javascript中,闭包是有问题的,因为变量是可变的,即它们可以更改从关闭到调用返回函数之间的值。
幸运的是,函数式语言中的变量是不可变的,从而消除了常见的错误和混乱。
我的脑子!!!!
现在足够了。
在本文的后续部分中,我将讨论功能组合,Currying,常用功能(例如,地图,过滤器,折叠等)等。
接下来:第3部分