第882期成为一名函数式码农系列之四

Posted 前端早读课

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第882期成为一名函数式码农系列之四相关的知识,希望对你有一定的参考价值。

前言

离上一次分享这个系列已经是好几个月之前了,3 月 22 号 早读文章由前端早读课专栏作者 @ 野草翻译分享。

正文从这开始~

柯里化(Currying)

还记得我们讲的吗?当mult5接受1个参数,而add接受2个参数时,mult5和add 的函数复合遇到了麻烦。

其实解决这个问题很简单,我们只需限定所有的函数只能接受1 个函数。

听起来有点难以接受?相信我,并没有你想得那么糟糕。

我们只需将接受2个参数的add函数写成每次只接受1个参数(下简称单参)的函数。那要怎么做呢?正巧,函数柯里化就是用来干这个的。

柯里化函数是每次只接受单参的函数。

我们先赋值add的第1个参数,然后再组合上mult5,得到mult5AfterAdd10函数。当mult5AfterAdd10函数被调用的时候,add得到了它的第2 个参数。

我们用javascript重写add 函数:

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

此时的add函数先后分两次得到第1个和第2个参数。具体地说,add函数接受单参x,返回一个也接受单参y的函数,这个函数最终返回x+y 的结果。

现在我们可以利用这个add函数来写出一个可行的mult5AfterAdd10 函数。

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

该复合函数接受2个函数,f和g,然后返回一个接受单参x的函数,当这个函数被调用时,f复合上g的映射就会应用在 x 上。

总结一下,我们到底做了什么?我们就是将简单常见的add函数转化成了柯里化函数,这样add函数就变得更加自由灵活了。我们先将第1个参数10输入,而当mult5AfterAdd10函数被调用的时候,最后1个参数才有了确定的值。

至此,你可能在思考怎么用Elm语言重写add函数。根本无需思考。在Elm这类的函数式语言中,所有的函数都默认是柯里化处理的。

所以,柯里化版本的add函数还是这样写(指跟上一篇提到的写法一致):

add x y =
   x + y

而在上一篇文章中,我们已经知道了mult5AfterAdd10函数的写法。

mult5AfterAdd10 =
   (mult5 << add 10)

从语法上来说,比起命令式语言(如JavaScript),Elm更胜一筹。因为它在函数式处理上进行了优化,比如柯里化,函数复合上。

柯里化与重构(Curring and Refactoring)

当你创建了一个接受很多参数的普适函数,然后想限制部分参数生成接受更少参数的一些特殊函数,此时柯里化大展拳脚,可以为你独当一面。

例如,我们有以下两个函数,它们分别将输入字符串用单花括号和双花括号包裹起来:

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

调用方式如下:

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

我们可以将bracket和doubleBracket推广为更普适的函数:

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

但每次我们调用generalBracket函数的时候,都得这么传参:

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

之前参数只需要输入1个,但定义了2个独立的函数;现在函数统一了,每次却需要传入3个参数。能不能鱼与熊掌兼得呢?

如果重新调整一下generalBracket函数的参数顺序,由于函数是可以被柯里化的,我们可以利用generalBracket函数创建出bracket函数和doubleBracket 函数:

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

只要将将最可能不变的参数即prefix和suffix放在最前面,把最可能变化的参数即 str放在最后面,我们很容易就能创建出generalBracket函数的特殊版本。

参数顺序是能否最大程度利用柯里化的关键所在。

同时注意一下,bracket函数和doubleBracket函数都是隐参标记式(point-free notation)写法,即参数 str是隐含的。它们都是单参数函数,等待着最后的参数输入。

现在我们可以像最开始那样调用了:

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

但,这次我们调用的是更普适的柯里化函数,generalBracket。

常见的函数式函数(Functional Function)

我们来看下函数式语言中3个常见的函数:Map,Filter,Reduce。

在这之前,先看如下JavaScript 代码:

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

这段代码存在一个很大的问题,但不是bug。问题在于它有很多重复代码(boilerplate code)。

如果你用命令式语言来编程,比如Java,C#,JavaScript,php,Python等等,你会发现这样的代码你写地最多。

这就是问题所在。

现在让我们来一起解决这个问题。将代码重写成一个(或者几个)绝对不需要for循环的函数。额,我是说,几乎不需要,至少在我们投入函数式语言之后。

先用名为things的数组来修改上述代码:

var things = [1, 2, 3, 4];
for (var i = 0; i < things.length; ++i) {
   things[i] = things[i] * 10; // 警告:值被改变 !!!!
}
console.log(things); // [10, 20, 30, 40]

什么?!数值被改变了!

我们再试一次,这次不会再修改things。

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,因为这个函数的功能就是将一个数组的每个值映射(map)到新数组的一个新值。

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

函数f作为参数传入,那么函数map可以对array数组的每项进行任意的操作。

现在我们可以使用map重写之前的代码:

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

瞧,没有for循环!而且代码更具可读性,也更易分析。

好吧,对技术上来说,函数map中还是存在for循环。至少现在不需要写一堆重复代码了。

我们再看另外一个常见的数组过滤(filter)函数是怎么写的:

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,否则返回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]

比起用for循环的手动编程(hand code),filter函数简单多了。

最后一个常见函数叫reduce。通常这个函数用来将一个数列归约(reduce)成一个数值,但事实上它能做很多事情。

在函数式语言中,这个函数称为fold。

var reduce = (f, start, array) => {
   var acc = start;
   for (var i = 0; i < array.length; ++i)
       acc = f(array[i], acc); // f() 接受2个参数
   return acc;
});

reduce函数接受一个归约函数f,一个初始值start,以及一个数组array。

归约函数f接受2个参数,array 的当前项,累加器acc。它在每次迭代中利用这些参数又产生新的累加器,直到迭代完成返回最后一次的累加器。

下面这个例子可以帮助我们理解具体的过程:

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

add函数接受2个参数,并将它们加起来。我们的归约函数正好也接受2个参数,所以add可以作为归约函数传入。

我们从start为0开始,传入待累加的数组values。reduce函数遍历数组values,每次迭代值都被累加,然后返回最后的累加值,并赋值给sumOfValues。

这三个函数,map,filter,reduce能让我们绕过for循环这种重复的方式,对数组做一些常见的操作。但在函数式语言中只有递归没有循环,这三个函数就更有用了。附带提一句,在函数式语言中,递归函数不仅非常有用,还必不可少。

点击查看了解《函数式攻城指南》


关于本文

译者:@野草

原文:https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-4-7005682cec4a#.s1r0k68u4

最近文章排版的小改变来自

@墨客编辑器,专为自媒体人开发的编辑器

以上是关于第882期成为一名函数式码农系列之四的主要内容,如果未能解决你的问题,请参考以下文章

第862期成为一名函数式码农之三

第888期成为一名函数式码农系列之六

第859期成为一名函数式码农之二

第858期成为一名函数式码农之一

疫情在家没事做推荐个学习的目录:怎么从一名码农成为架构师的必看知识点:目录大全(不定期更新)

PHP怎么从一名码农成为一名资深架构师