如何成为一位函数式编程爱好者(Part 2)

发布于 Heng温

踏出理解函数式编程概念的第一步是最重要的一步,有时也是最难的一步。不过也不一定,取决于你们的思考方式。

友情提示

请仔细通读示例代码。确保自己的确看懂了。每一节都是在上一节的基础上进行的。

如果你心急跳过了,对后面部分的理解就可能会出现偏差。

重构

花一分钟时间考虑一下如何重构下面的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这样的写法,我们应该创建一个单一函数,将粘复制贴后需要修改的部分参数化。

这个例子中,我们将参数化被验证值,正则表达式和打印的信息(至少是打印信息的最后一部分)。

重构后的代码:

function validateValue(value, regex, type) {
    if (regex.exec(value))
        console.log('Invalid ' + type);
    else
        console.log('Valid ' + type);
}

原来代码中的ssnphone现在都通过value表示。

正则表达式*/^\d{3}-\d{2}-\d{4}$/*和*/^\(\d{3}\)\d{3}-\d{4}$/*使用regex表示。

最后,返回信息最后的SSNPhone Number通过type表示。

维护一个函数比维护两个,三个,四个或十个要好很多。这样可以保证代码的整洁性和可维护性。

举个例子,如果发现了一个bug,你只需要在一处做修改,而不用从全部代码中去找这个函数可能还粘贴到了哪里后再处处修改。

但如果是下面的场景:

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');
}

现在*parseAddressparseFullName是接收字符串的函数,如果验证成功则返回true*。

我们该如何重构?

我们可以像之前一样用value表示addressname,用type表示AddressName,但是之前的正则表达变成函数了。

要是能将函数作为参数传入就好了。。。

高阶函数

很多语言不支持传入函数作为参数。有的支持但使用起来很麻烦。

在函数式编程中,函数是语言中的一等公民。换言之,函数只是另一种值。

因为函数就是值,所以可以作为参数传入。

虽然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');

比以前好多了。现在想要验证一个电话号码,不必再复制粘贴正则表达式了。

但如果我们有更多地正则表达式用来解析,不只是parseSsnparsePhone。每次创建一个新的正则表达式,都要记得在结尾加上.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

parseSsnparsePhone和之前的作用一样,是正则表达式的exec方法。

诚然这是个微小的提升,重点是这是一个返回函数的高阶函数的例子。

不过,你可以想象一下如果makeRegexParser更复杂,优化效果就更明显了。

下面是另一个返回函数的高阶函数的例子:

function makeAdder(constantValue) {
    return function adder(value) {
        return constantValue + value;
    };
}

makeAdder接收一个常量后返回adder函数,add函数会返回接收到的参数和这个常量相加后的结果。

可以像下面这样使用:

var add10 = makeAdder(10);
console.log(add10(20)); // prints 30
console.log(add10(30)); // prints 40
console.log(add10(40)); // prints 50

我们通过向makeAdder传入常量10创建了一个函数add10,这个函数就会对任何东西加上10

注意adder函数即使被makeAdder函数返回了,它仍然能获取makeAdder的常量。因为当adder创建时,这个常量就在它的作用域中了。

这个现象非常重要,如果没有它,返回函数的函数不会有很大用途。所以理解这种现象是如何产生,这种现象叫做什么是很重要的。

这种现象叫做闭包。

闭包

这有一个闭包的例子:

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

parentFuncgrandParent返回后保持parent的作用域为激活状态。

同样的,childFuncparentFunc返回后保持child的作用域为激活状态。

当一个函数创建后,在这个函数的生命周期中所有其作用域下的变量都是可访问的。只要有地方引用它了,这个函数就会存在。例如只要childFunc还保持child作用域的引用,它就会一直存在。

闭包是通过引用其他函数来保持该函数作用域的现象。

注意,在JavaScript中变量会突变,使用闭包时可能会有麻烦。即从形成闭包到返回函数被调用这段时间里,变量的值可能会被改变。

万幸的是,函数式语言中变量是不可变的,排除了这种bug和困惑的主要来源。

记在脑子里

这次就到这。

在本文后续部分,我们将一起讨论函数组合、柯里化、常用函数方法等等,感兴趣的同学请继续关注后续相关更新。

本文根据@Charles Scalfani的《So You Want to be a Functional Programmer (Part 2)》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-2-7005682cec4a#.nhbe1yroz

Heng温

前端开发,音乐,动漫,技术控,喜欢折腾新鲜事物,以不断学习的态度,在前端的路上走下去。

如需转载,烦请注明出处:https://www.fedev.cn/javascript/so-you-want-to-be-a-functional-programmer-part-2.htmlAir Force 1 Ultra Flyknit 2.0 - Black