听说你想成为一名函数式编程工程师(第二部分)

栏目: 编程语言 · 发布时间: 5年前

理解函数式编程的概念是重要的第一步,也可能是最困难的一步。但不是说就一定得从概念起步。不妨换个适合的视角。

上一篇:第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) 正则表达式(regex) 和打印的 消息(message) (至少是输出消息的最后一部分)。

重构后的代码:

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’ ‘Phone Number’ 由参数  type  传入。

用一个函数比用两个函数好得多,就更不用说代替三、四个,甚至十个函数了。这会让你的代码整洁且易于维护。

比如说,如果存在 BUG,你只需要修改一个地方,而不是在整个代码库中搜索这个函数可能被在哪些方被粘贴修改过。

但是如果遇到下面这样的情况该怎么办:

function validateAddress(address) {  
    if (parseAddress(address))  
        console.log('Valid Address');  
    else  
        console.log('Invalid Address');  
}

function validateName(name) {  
    if (arseFullName(name))  
        console.log('Valid Name');  
    else  
        console.log('Invalid Name');  
}

这里 parseAddress parseFullName 都是需要一个 string 参数的函数,而且如果解析成功都返回 true

该如何重构呢?

我们可以像之前那样,把 address name 作为 value 传入,而 'Address' 'Name' 作为 type ,然后在传入正则表达式的地方传入函数。

既然我们可以把函数作为参数传入,那还有啥好说的……

高阶函数

许多语言并不支持将函数作为参数传递。一些(语言)虽然支持,但过程繁琐。

在函数式编程中,函数便是该语言一等公民。换言之,一个函数只是另一种值的表现方式。

因为函数只是一些值而已,那么我们便可把它们当做参数进行传递。

尽管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 **函数,该函数接受一个字符串。 validateValueWithFuncwill 将字符串 value 传递给parse函数,即 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 返回后,函数 adder 也可以访问 constantValue 。那是因为当创建 adder 时, constantValue 在其作用域之内。

这种行为非常重要,因为如果没有它,返回函数的函数将不会非常有用。因此,重要的是我们要了解它们的工作方式以及此类行为的术语。

这种行为被称为 Closure

Closures 闭包

这是一个使用闭包的函数的人为设计的例子:

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 的变量。

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

这里,在 grandParent 返回 parent 之前, parentFunc 将在 parent 作用域内有效。

同样地,在 parentFunc, 亦即 parent 返回 child 之前, childFunc 将在 child 作用域内有效。

创建函数时,在函数生命周期内,它可以访问在其创建时其作用域内的所有变量。只要仍然存在对某函数的引用,该函数就是存在的。例如,只要 childFunc 仍引用 child ,那么它的作用域就是存在的。

闭包是一个函数的作用域,它通过对该函数的引用保证其可见性。

请注意,在Javascript中,闭包是存在问题的,因为变量是可变的,即它们可以在封闭它们到调用返回函数的时间内改变值。

值得庆幸的是,函数式语言中的变量是不可变的,这规避了这种常见的错误和混淆源。

我的脑袋!!!!

到现在为止足够了。

在本文的后续文章中,我将探讨函数式组合、Currying、通用函数式函数(例如地图、过滤器、折叠等)等内容。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Powerful

Powerful

Patty McCord / Missionday / 2018-1-25

Named by The Washington Post as one of the 11 Leadership Books to Read in 2018 When it comes to recruiting, motivating, and creating great teams, Patty McCord says most companies have it all wrong. Mc......一起来看看 《Powerful》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

html转js在线工具
html转js在线工具

html转js在线工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具