第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期成为一名函数式码农系列之四的主要内容,如果未能解决你的问题,请参考以下文章