转载

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

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

柯里化

如果你还记得Part 3的内容,我们在组合 mult5add 的时候遇到了问题,因为 mult5 接收一个参数而 add 接收两个。

只要限制所有函数都只接收一个参数,就可以轻松地解决这个问题。

相信我。这没听上去那么糟。

我们创建一个 add 函数,它接收两个参数,但是每次只接收一个参数。柯里化函数允许我们这么做。

柯里化函数是一种每次只接收一个参数的函数。

它允许我们在 addmult5 组合前先传入它的第一个参数。

JavaScript里,我们可以这样实现 add 函数:

var add = x => y => x + y

这个版本的 add 是一个先接收一个参数,之后再接收另一个参数的函数。

具体来说,这个 add 接收单一参数 x ,然后返回一个接收单一参数 y 的函数,这个函数最终返回 xy 的加和。

现在可以用这个版本的 add 来构建一个可用的 mult5AfterAdd10 了:

var compose = (f, g) => x => f(g(x));
var mult5AfterAdd10 = compose(mult5, add(10));

compose 函数接收两个参数, fg 。返回一个接收一个参数 x 的函数, x 会先传入 g 执行后,结果再传入 f

实际上我们做了什么?我们将旧的 add 函数转换成了柯里化版本。让 add 更加灵活,因为它的第一个参数 10 可以提前传入,最后一个参数可以在 mult5AfterAdd10 调用时再传入。

现在,你可能想知道在 Elm 中这个 add 函数要如何重写。事实证明,根本不需要重写。在 Elm 和其它函数式编程语言中,所有函数都是自动柯里化的。

所以 add 函数看上去和之前一样:

add x y =
    x + y

Part 3 中的 mult5AfterAdd10 需要这样重写:

mult5AfterAdd10 =
    (mult5 << add 10)

从语法上讲, Elm 要优于像JavaScript这样的指令式语言,因为它对柯里化,函数组合等函数式的东西做了优化。

柯里化和重构

柯里化的另一个好处则体现在重构时,创建一个接收多参数的通用版函数,然后在需要用的地方通过传入部分参数创建出指定版本的函数。

举个例子,当我们需要下面两个将括号和双括号添加在字符串两侧的函数时:

bracket str =
    "{" ++ str ++ "}"
doubleBracket str =
    "{{" ++ str ++ "}}"

我们需要这么用:

bracketedJoe =
    bracket "Joe"
doubleBracketedJoe =
    doubleBracket "Joe"

我们可以将 bracketdoubleBracket 通用化:

generalBracket prefix str suffix =
    prefix ++ str ++ suffix

但现在每次调用 generalBracket 时都必须将括号传入:

bracketedJoe =
    generalBracket "{" "Joe" "}"
doubleBracketedJoe =
    generalBracket "{{" "Joe" "}}"

我们需要两全其美的办法。

如果把 generalBracket 的参数重新排序,我们可以利用函数自动柯里化的特点创建出 bracketdoubleBracket

generalBracket prefix suffix str =
    prefix ++ str ++ suffix
bracket =
    generalBracket "{" "}"
doubleBracket =
    generalBracket "{{" "}}"

注意,将静态参数( prefixsuffix )放在参数列表靠前的位置,可能改变的参数( str )放在后面,就可以轻松地创建出指定版本的 generalBracket

参数顺序对完全柯里化很重要。

还有一点, bracketdoubleBracketpoint-free 模式的,即 str 参数是隐式的。 bracketdoubleBracket 都在等待它们的最后一个参数。

现在可以像之前一样使用了:

bracketedJoe =
    bracket "Joe"
doubleBracketedJoe =
    doubleBracket "Joe"

但这次我们使用了通用化的柯里化函数 generalBracket

常用的功能函数

让我们看一下函数式语言里最常用的三个函数。

但在那之前,先看一眼下面这段JavaScript代码:

for (var i = 0; i < something.length; ++i) {
    // do stuff
}

这段代码有一个很严重的问题。但不是bug。问题在于这段代码其实是模板代码,即一遍一遍被重复的代码。

如果你用指令式语言,像Java, C#, JavaScript, PHP, Python等,就会发现这样的模板代码到处都是。

这就是它的错误所在。

所以让我们扼杀它。

从改变数组 things 开始:

var things = [1, 2, 3, 4];
for (var i = 0; i < things.length; ++i) {
    things[i] = things[i] * 10; // MUTATION ALERT !!!!
}
console.log(things); // [10, 20, 30, 40]

呃!!突变!

再试一次。这次我们不改变东西:

var things = [1, 2, 3, 4];
var newThings = [];
for (var i = 0; i < things.length; ++i) {
    newThings[i] = things[i] * 10;
}
console.log(newThings); // [10, 20, 30, 40]

好了,现在我们没有改变 things 但从技术上讲,我们改变了 newThings 。先不去管它。毕竟是在用JavaScript。一旦迁移到一个函数式语言,我们就再也不能进行突变操作。

这部分的重点在于理解这些函数是如何工作的,并使用它们减少糟糕的代码。

把这些代码放在一个函数中。我们将使用第一个常用函数 map ,它操作旧数组中的每个值,并返回一个新数组。

var map = (f, array) => {
    var newArray = [];
    for (var i = 0; i < array.length; ++i) {
        newArray[i] = f(array[i]);
    }
    return newArray;
};

注意函数 f ,它被传入 map 中了,所以我们可以通过 f 对数组中的每个值进行任意操作。

现在可以用 map 重写之前的代码了:

var things = [1, 2, 3, 4];
var newThings = map(v => v * 10, things);

看啊。没有 for 循环。也因此增强了可读性。

当然,从技术上讲, map 函数里也是 for 循环。但至少我们不用再一遍一遍地写模板代码了。

现在来实现另一个常用函数来从数组中过滤值:

var filter = (pred, array) => {
    var newArray = [];
for (var i = 0; i < array.length; ++i) {
        if (pred(array[i]))
            newArray[newArray.length] = array[i];
    }
    return newArray;
};

注意判断函数 pred ,返回 TRUE 则保留 item ,丢弃则返回 FALSE

下例展示了 filter 如何从数组中过滤出奇数:

var isOdd = x => x % 2 !== 0;
var numbers = [1, 2, 3, 4, 5];
var oddNumbers = filter(isOdd, numbers);
console.log(oddNumbers); // [1, 3, 5]

使用新的 filter 函数比在 for 循环中进行逻辑判断更简单。

最后一个常用函数叫 reduce 。通常来说,它用来将一个 list 缩减成一个单一值,但实际上它可以做更多事情。

在函数式语言中这个函数也叫 fold

var reduce = (f, start, array) => {
    var acc = start;
    for (var i = 0; i < array.length; ++i)
        acc = f(array[i], acc); // f() takes 2 parameters
    return acc;
});

reduce 函数接收一个缩减函数 f ,一个初始值 start 和一个数组。

注意缩减函数 f ,它接收两个参数,一个数组当前值,一个累加器 acc 。每次迭代 f 用这两个参数产生一个新的累加器。最终一次迭代完成后累加器被返回。

下面的例子可以帮助我们理解它如何工作:

var add = (x, y) => x + y;
var values = [1, 2, 3, 4, 5];
var sumOfValues = reduce(add, 0, values);
console.log(sumOfValues); // 15

注意 add 函数接收两个参数然后相加。我们的 reduce 期望接收一个接收两个参数的函数,它们才能很好的配合。

我们传入 0 作为起始值和传入数组的值进行加和。在 reduce 函数内部, sum 随着迭代器累加。最终的累加值返回给 sumOfValues

map , filter , reduce 中让我们可以对数组进行常用操作而不必重复写模板代码。

但在函数式语言中,他们更常用,因为在函数式语言中没有循环结构,只有递归。迭代器函数不只是极其有用。而且是必须的。

记在脑子里!!!!

这次先到这里。

在这个系列后面的部分,将要讨论引用完整性,执行顺序,类型等。

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

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

Heng温

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

原文  http://www.w3cplus.com/javascript/so-you-want-to-be-a-functional-programmer-part-4.html
正文到此结束
Loading...