准备充分了嘛就想学函数式编程?(Part 2)

本文讲的是准备充分了嘛就想学函数式编程?(Part 2),


想要理解函数式编程,第一步总是最重要,也是最困难的。但是只要有了正确的思维,其实也不是太难。

之前的部分: 第一部分

友情提示

请读仔细读代码,确保继续之前你已经理解。每一代码段落都基于它之前的代码。

如果你太急,可能会遗漏一些重要的细节。

重构

让我们先来重构一段 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);
}

旧代码中要验证的参数 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 (parseFullName(name))
        console.log('Valid Name');
    else
        console.log('Invalid Name');
}

这里 parseAddress 和 parseFullName 函数都只接受一个字符串参数,并在符合解析条件时返回 true 。

我们怎样重构这段代码?

我们可以用 value 来代替 address 和 name, 用 type 来替换 'Address' 和 'Name',就像我们之前那样,但之前是将正则表达式作为参数传入,现在是函数。

如果我们能把一个函数当作参数传入就好了。。。

高阶函数

很多语言并不支持将函数作为参数传入。一些语言虽然支持,但用起来不直观。

在函数式编程中,函数是语言的第一公民。换句话说,函数就是另一种值。

因为函数是值,我们可以把它们当作参数传入函数。

尽管 JavaSscript 不是一门纯函数式语言,你也可以用它做一些函数式操作。我们可以将之前的两个函数重构成一个叫 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 可以传入字符串,值,给 parse 函数,例如 exec 。

parseSsn 和 parsePhone 和之前用正则表达式的 exec 函数一样可用。

的确,这只是一个微小的提升,但这里向我们展示了高阶函数将函数作为返回值返回的例子。

不过你可以想象如果 makeRegexParser 更复杂,这样改动可以给我们带来的好处。

这是另一个高阶函数返回函数作为返回值的例子:

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

这里 makeAddr 函数接受一个参数 constantValue,返回一个函数 addr,它的返回是 contantValue 与它接受的任意值相加的结果。

它的用法是:

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

我们通过将 10 作为参数传给 makeAddr,创建了 add10 函数,它接受任意值作为参数,并与 10 求和返回。

需要注意的是,,即使在 makeAddr 返回后,函数 addr 仍可以获取到 constantValue 参数的值。这是因为 constantValue 在 addr 函数被创建时的作用域中。

这种行为非常重要,因为如果不是这样,将函数作为返回值返回的函数就没有多大用处了。所以我们理解它的工作原理非常重要。

这种行为叫做闭包。

闭包

这有一个故意使用闭包的函数:

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

这里,parentFunc 可以保持 parent 函数的作用域,因为 grandParent 将 parent 作为返回值返回。

类似的,childFunc 可以保持 child 函数的作用域,因为 parentFunc 其实是返回 child 函数的 parent 函数。

当创建一个函数时,创建时所处的作用域的所有变量都是可以读取的。如果函数仍被引用,作用域保持存活状态。例如 child 函数的作用域只要 childFunc 的引用存在,就算存活。

闭包指函数通过被引用,保持其作用域的存活状态。

注意在 JavaScript 中,因为变量是可变的,所以闭包可能会引入问题。例如这些变量可能从它们被闭包开始到函数返回的周期里被修改。

值得庆幸的是,函数式语言中的变量是不可变的,所以就可以消除这种常见的错误和混乱。

我的脑子!

到目前暂时足够消化一段了。

在文章接下来的部分里,我会涉及到 函数组合,柯里化,函数式编程中常见的函数(如 map,filter,fold 等)





原文发布时间为:2016年11月17日


本文来自合作伙伴掘金,了解相关信息可以关注掘金网站。

时间: 2024-09-20 15:47:07

准备充分了嘛就想学函数式编程?(Part 2)的相关文章

准备充分了嘛就想学函数式编程?(第一部分)

本文讲的是准备充分了嘛就想学函数式编程?(第一部分), 迈出理解函数式编程概念的第一步是最重要的,有时也是最难的一步.但是不一定特别难.只要选对了思考方法就不难. 学开车 第一次学车时,我们也曾挣扎过.看别人学开车时觉得真的很简单.但事实上学车比我们想象的难多了. 我们借父母的车子练习,在家周围街道上开熟练之前甚至都不敢冒险开到公路上去. 但是通过不断的练习,在经历过一些父母想忘掉的担心令人的经历之后,我们学会了开车,最终拿到了驾照. 拿到驾照之后我们一有机会就会把车开出去.每次出行都会让我们的

准备充分了嘛就想学函数式编程?(第五部分)

本文讲的是准备充分了嘛就想学函数式编程?(第五部分), 迈出理解函数式编程概念的第一步是最重要的,有时也是最难的一步.但是不一定特别难.只要选对了思考方法就不难. 前几部分: 第一部分, 第二部分, 第三部分, 第四部分 引用透明 引用透明 是一个很酷炫的术语,它指的是一个纯函数能够安全地被它的表达式所替代.下面用一个例子来解释这个术语. 在代数中当你有以下这个公式时: y = x + 10 并且已知: x = 3 你可以将 x 代入方程来得到: y = 3 + 10 此时这个方程依旧成立.我们

准备充分了嘛就想学函数式编程?(第四部分)

本文讲的是准备充分了嘛就想学函数式编程?(第四部分), 想要理解函数式编程,第一步总是最重要,也是最困难的.但是只要有了正确的思维,其实也不是太难. 之前的部分: 第一部分, 第二部分, 第三部分 柯里化 如果你还记得第三部分内容的话,就会知道我们在组合 mult5 和 add 这两个函数时遇到问题的原因是:mult5 接收一个参数而 add 却接收两个. 其实只需要通过限制所有函数都只接收一个参数,就可以轻易地解决这个问题. 相信我,这并没有听起来那么糟糕. 我们只需要来写一个使用两个参数,但

准备充分了嘛就想学函数式编程?(Part 6)

本文讲的是准备充分了嘛就想学函数式编程?(Part 6), 第一步,理解函数式编程概念是最重要的一步,同时也是最难的一步.如果你从正确的角度或方法来理解的话,它也未必会有那么难. 回顾之前的部分: Part 1, Part 2, Part 3, Part 4, Part 5 现在该做什么? 现在你已经学会了所有这些新东西了,你可能在想,"现在该干什么?我如何在日常编程中使用它?" 这得看情况.如果你会使用纯函数式语言(如 Elm 或 Haskell)编程,那么你可以尝试所有这些想法.这

想学jsp web编程,各位大虾有好意见么

问题描述 想学jspweb编程,各位大虾有好意见么 解决方案 解决方案二:多动手敲代码,多做项目解决方案三:做两个jsp+servlet+JavaBean的网站.熟悉mvc框架的结构.看下jsp,servlet相关书籍,struts,spring,hibernate相关书籍解决方案四:从jspservletjdbc整起解决方案五:我是学java方面的,jspweb我觉得是需要发一点时间的,现在学的话都是关于SSH编程的.建议下点视频看看.解决方案六:LZ基础是主要...先抓起基础.然后在学习框架

《JavaScript函数式编程》读后感_javascript技巧

本文章记录本人在学习 函数式 中理解到的一些东西,加深记忆和并且整理记录下来,方便之后的复习. 在近期看到了<JavaScript函数式编程>这本书预售的时候就定了下来.主要目的是个人目前还是不理解什么是函数式编程.在自己学习的过程中一直听到身边的人说面向过程编程和面向对象编程,而函数式就非常少.为了自己不要落后于其他同学的脚步,故想以写笔记的方式去分享和记录自己阅读中所汲取的知识. js 和函数式编程 书中用了一句简单的话来回答了什么是函数式编程: 函数式编程通过使用函数来将值转换为抽象单元

我想学计算机-想学计算机!从根本学起

问题描述 想学计算机!从根本学起 谁知道有什么书介绍了计算机的起源及发展和原理,还有汇编语言,c语言的原理 解决方案 要想学计算机,关键是要有一个系统的过程.大家都知道,计算机是美国人发明的,所以要学计算机需要看原版的高质量的书籍.看了不对的书,就要走冤枉路.像楼下的书,难度就比较大,不适合lz.姐姐有一些很好的入门的书,介绍各种原理的.都是金针度人的好书.lz如果采纳了姐姐的回答(方法是点击姐姐回答右边的采纳按钮),姐姐发给你.祝你好运. 解决方案二: 要想学计算机,关键是要系统的学习.1.硬

学做界面#-想学做界面的信息安全专业的会敲代码的色影丝小学渣

问题描述 想学做界面的信息安全专业的会敲代码的色影丝小学渣 自身具备的艺术素养对做出优质的界面有助推作用吗?我对别人做的界面的构图位置美观吧啦吧啦很敏感,脑中会形成一个自己感觉更舒服的界面版式,这对做出优质的界面有助推作用吗,还是小学生胡思乱想了,第一次提问,求指示,轻喷. 解决方案 有艺术细胞是好事.但是如果过于强调这个,而忽略了系统的学习.理性和理论,那么是没什么好处的. 一个靠直觉和自发得到的经验而进行界面设计的人,可能你能设计用户群体和你本人有着相同背景的简单的小软件. 但是一个前端交互

jsp-下边的代码谁懂啊,大神求解释。顺道告诉我下想学关于这个看什么,谢谢啦

问题描述 下边的代码谁懂啊,大神求解释.顺道告诉我下想学关于这个看什么,谢谢啦 <head> <jsp:include page='/res/inc/inc.jsp' flush='true'/> <style> .FixedTitleRow { position: relative; top: expression( this.offsetParent.scrollTop ); z-index: 10; background-color: #ffffff; } .Fi