加载中

Taking that first step to understanding Functional Programming concepts is the most important and sometimes the most difficult step. But it doesn’t have to be. Not with the right perspective.

Previous parts: Part 1

Friendly Reminder

Please read through the code slowly. Make sure you understand it before moving on. Each section builds on top of the previous section.

If you rush, you may miss some nuance that will be important later.

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

上一篇:第1部分

友情提示

请慢慢地阅读代码,确保你能理解他们。本文的每一节都依赖于上一节的内容。

如果你过于着急,就可能错过一些重要的细节。

Refactoring

Let’s think about refactoring for a minute. Here’s some Javascript code:

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

We’ve all written code like this before and over time, we start to recognize that these two functions are practically the same and only differ by a few things (shown in bold).

Instead of copying validateSsn and pasting and editing to create validatePhone, we should create a single function and parameterize the things that we edited after pasting.

重构

让我们花点时间思考一下重构。这里有一段 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,我们需要创建一个函数,粘贴内容并进修改,使之参数化。

In this example, we would parameterize the value, the **regular expression**and the message printed (at least the last part of the message printed).

The refactored code:

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

The parameters ssn and phone in the old code are now represented by value.

The regular expressions /^\d{3}-\d{2}-\d{4}$/ and /^(\d{3})\d{3}-\d{4}$/ are represented by regex.

And finally, the last part of the message ‘SSN’ and ‘Phone Number’ are represented by type.

Having one function is much better than having two functions. Or worse three, four or ten functions. This keeps your code clean and maintainable.

在这个例子中,可以抽象出值(value)正则表达式(regex) 和打印的消息(message)(至少是输出消息的最后一部分)。

重构后的代码:

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 传入。

最后一,消息的后面部分 ‘SSN’‘Phone Number’ 由参数 type 传入。

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

For example, if there’s a bug, you only have to fix it in one place versus searching through your whole codebase to find where this function MAY have been pasted and modified.

But what happens when you have the following situation:

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

Here parseAddress and parseFullName are functions that take a string and return true if it parses.

How do we refactor this?

Well, we can use value for address and name, and type for ‘Address’ and**‘Name’** like we did before but there’s a function where our regular expression used to be.

If only we could pass a function as a parameter…

比如说,如果存在 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,然后在传入正则表达式的地方传入函数。

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

Higher-Order Functions

Many languages do not support passing functions as parameters. Some do but they don’t make it easy.

In Functional Programming, a function is a first-class citizen of the language. In other words, a function is just another value.

Since functions are just values, we can pass them as parameters.

Even though Javascript is not a Pure Functional language, you can do some functional operations with it. So here’s the last two functions refactored into a single function by passing the parsing function as a parameter called parseFunc:

function validateValueWithFunc(value, parseFunc, type) {  
    if (parseFunc(value))  
        console.log('Invalid ' + type);  
    else  
        console.log('Valid ' + type);  
}

Our new function is called a Higher-order Function.

Higher-order Functions either take functions as parameters, return functions or both.

Now we can call our higher-order function for the four previous functions (this works in Javascript because Regex.exec returns a truthy value when a match is found):

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

This is so much better than having four nearly identical functions.

高阶函数

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

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

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

尽管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');

这样就比有四个类似的独立函数要好多了。

But notice the regular expressions. They’re a bit verbose. Let’s clean up our a code by factoring them out:

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

That’s better. Now when we want to parse a phone number, we don’t have to copy and paste the regular expression.

But imagine we have more regular expressions to parse, not just parseSsn and parsePhone. Each time we create a regular expression parser, we have to remember to add the .exec to the end. And trust me, this is easy to forget.

We can guard against this by creating a high-order function that returns the exec function:

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

Here, makeRegexParser takes a regular expression and returns the **exec**function, which takes a string. validateValueWithFunc will pass the string, value, to the parse function, i.e. exec.

parseSsn and parsePhone are effectively the same as before, the regular expression’s exec function.

但请注意正则表达式。 他们有点冗长。 让我们通过正则解析来清理下我们的代码:

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 **函数,该函数接受一个字符串。 validateValueWithFuncwill将字符串value传递给parse函数,即exec

parseSsnparsePhone实际上和以前一样,是正则表达式的exec函数。

Granted, this is a marginal improvement but is shown here to give an example of a high-order function that returns a function.

However, you can imagine the benefits of making this change if makeRegexParser was much more complex.

Here’s another example of a higher-order function that returns a function:

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

Here we have makeAdder that takes constantValue and returns adder, a function that will add that constant to any value it gets passed.

当然,这是一个微小的改进,但放到这里是为了给出一个返回函数的高阶函数的示例。

但是,如果makeRegexParser更复杂的话,你可以想象下做如此更改的好处。

这是返回函数的高阶函数的另一个示例:

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

这里我们定义了makeAdder,它接收constantValue作为参数并返回adder ——一个可以将传递给它的任意值加上给定常量的函数。

Here’s how it can be used:

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

We create a function, add10, by passing the constant 10 to makeAdder which returns a function that will add 10 to everything.

Notice that the function adder has access to constantValue even after makeAddr returns. That’s because constantValue was in its scope when adder was created.

This behavior is very important because without it, functions that return functions wouldn’t be very useful. So it’s important we understand how they work and what this behavior is called.

This behavior is called a Closure.

下面是它是如何被使用的示例:

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

Here’s a contrived example of functions that use 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;  
        };  
    };  
}

In this example, child has access to its variables, the parent’s variables and the grandParent’s variables.

The parent has access to its variables and grandParent’s variables.

The grandParent only has access to its variables.

(See pyramid above for clarification.)

Here’s an example of its use:

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


Here, parentFunc keeps the parent’s scope alive since grandParent returns parent.

Similarly, childFunc keeps the child’s scope alive since parentFunc, which is just parent, returns child.

When a function is created, all of the variables in its scope at the time of creation are accessible to it for the lifetime of the function. A function exists as long as there still a reference to it. For example, child’s scope exists as long as childFunc still references it.

A closure is a function’s scope that’s kept alive by a reference to that function.

Note that in Javascript, closures are problematic since the variables are mutable, i.e. they can change values from the time they were closed over to the time the returned function is called.

Thankfully, variables in Functional Languages are Immutable eliminating this common source of bugs and confusion.

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中,闭包是存在问题的,因为变量是可变的,即它们可以在封闭它们到调用返回函数的时间内改变值。

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

My Brain!!!!

Enough for now.

In subsequent parts of this article, I’ll talk about Functional Composition, Currying, common functional functions (e.g map, filter, fold etc.), and more.

我的脑袋!!!!

到现在为止足够了。

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

返回顶部
顶部