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

栏目: 编程语言 · 发布时间: 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、通用函数式函数(例如地图、过滤器、折叠等)等内容。


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

查看所有标签

猜你喜欢:

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

现代应用数学手册

现代应用数学手册

《现代应用数学手册》编委会 / 清华大学出版社 / 2005-1-1 / 48.00元

本书是进行科学计算的常备工具书,内容新颖,查阅方便,实用性强。主要介绍生产、科研、管理、数学等实践中在计算机上使用的各种计算方法和技巧。全书分为14章,依次为数值计算概论、插值法、函数逼近与曲线拟合、数值积分与数值微分、方程求根、线性方程组的直接解法和迭代解法、矩阵特征值问题、非线性方程组数值解与最优化方法、常微分方程初值问题和边值问题的数值解法、偏微分方程的数值解法、多重网络法和积分方程数值解法......一起来看看 《现代应用数学手册》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具