踏出理解函数式编程概念的第一步是最重要的一步,有时也是最难的一步。不过也不一定,取决于你们的思考方式。
函数组合
身为程序员,我们很懒。不想在写完代码后一遍又一遍地构建,测试,发布。
我们总是在寻找只写一次然后在其它地方多次复用的方法。
代码复用听上去很棒但实现起来很难。代码太定制化就不能重用,太通用化的代码首先就很难用。
所以我们需要的是平衡两者的方法,将小的,可重用代码块像积木一样通过组合来实现更复杂的功能。
在函数式编程中,函数就是我们的积木。我们可以创建它们来完成指定的任务并像乐高积木一样将它们组合在一起。
这就叫函数组合。
如何实现呢?通过下面的两个函数看一下:
var add10 = function(value) {
return value + 10;
};
var mult5 = function(value) {
return value * 5;
};
代码太冗长了,用箭头函数重写一下:
var add10 = value => value + 10;
var mult5 = value => value * 5;
看上去好多了。现在如果还需要一个函数,接收一个参数然后先加10
,然后再讲结果乘以5
。可以这么写:
var mult5AfterAdd10 = value => 5 * (value + 10)
即使这是一个很简单的函数,我们也不想把这个函数重新写一遍。首先,我们可能会犯忘写括号这种错误。
其次,我们已经有了一个加10
和一个乘5
的函数。我们是在重复之前的工作。
所以换个方法,用add10
和mult5
来构造新函数:
var mult5AfterAdd10 = value => mult5(add10(value));
我们仅用已存在的函数创建了mult5AfterAdd10
,但是有更好的方法。
在数学中,f ∘ g
是函数组合,读作f
和 g
的复合函数,更通俗地说,“f
在 g
之后调用”,所以(f ∘ g)(x)
等价于先调用g
函数再调用f
函数,即f(g(x))
。
我们的例子中,我们需要mult5 ∘ add10
,或者叫“mult5在
add10后调用”,所以新函数的名字叫
mult5AfterAdd10`。
我们也是这么做的。在调用完add10
后再调用mult5
,mult5(add10(value))
。
JavaScript没直接地支持函数组合,看在Elm
中代码时什么样子的:
add10 value =
value + 10
mult5 value =
value * 5
mult5AfterAdd10 value =
(mult5 << add10) value
<<
操作符在Elm
中用来组合函数。它让我们直观地看出数据的流向。首先,值传到了add10
,然后它的结果传入mult5
。
注意mult5AfterAdd10
括号里的部分,即(mult5 << add10)
。加括号确保函数组合是在调用之前。
你可以按你喜欢的方式组合更多函数:
f x =
(g << h << s << r << t) x
这里x
传入函数t
,然后结果传入r
,r
的结果再传入s
以此类推。类比到JavaScript就像g(h(s(r(t(x)))))
,括号噩梦。
Point-Free 风格
不明确指定参数的代码风格叫做Point-Free风格
。最开始看到这种风格可能会感觉古怪,但是慢慢地你就会感觉到它的简洁。
在mult5AfterAdd10
中,可以发现value
指定了两次。一次在参数列表,一次在它被传入的地方。
-- This is a function that expects 1 parameter
mult5AfterAdd10 value =
(mult5 << add10) value
但这个参数没必要声明,因为add10
,函数组合中最右面的函数,接收相同的参数。等价的point-free
版本:
-- This is also a function that expects 1 parameter
mult5AfterAdd10 =
(mult5 << add10)
point-free
版本有很多好处。
首先,不需要声明冗余的参数。因为没必要,也不再需要绞尽脑汁地为它们命名。
其次更少的冗余代码让函数可读性更好。这个例子比较简单,但是在接收更多参数的函数中就明显了。
天堂里的烦恼
到现在我们已经知道函数组合如何工作了,也知道了Point-Free风格
能让函数更简洁,清晰,灵活。
现在试着将这一思想用在稍微不同的环境中看看会发生什么。假如将add10
替换成add
:
add x y =
x + y
mult5 value =
value * 5
如何使用这两个函数实现mult5After10
?
继续读之前先思考一下。想一下,试着实现以下。
如果你的确花时间思考这个问题了,你可能会想到这个方法:
-- This is wrong !!!!
mult5AfterAdd10 =
(mult5 << add) 10
但这是错的。为什么?因为add
接收两个参数。
在Elm
中不明显,放到Javascript中看一下:
var mult5AfterAdd10 = mult5(add(10)); // this doesn't work
这段代码是错的,为什么?
因为add
函数只接收到了两个参数中的一个,所以错误的计算结果传入mult5
。最终产生的结果也是错的。
事实上在Elm
中,编译器不会让你写出这种残缺的代码(这也是Elm
的优点之一)。
再试一次:
var mult5AfterAdd10 = y => mult5(add(10, y)); // not point-free
这个函数不是point-free
风格但可用。但这不再是函数组合了。我写了一个新函数。同样,如果它变得更复杂,比如我想在mult5AfterAdd10
上在组合其他函数,将陷入麻烦。
所以可以看出函数组合可用性很有限,因为我们不能将这两个函数组合在一起,太糟糕了。
如何解决这个问题?需要用什么来解决?
如果有方法可以让add
函数先接收一个参数,另外一个参数在mult5AfterAdd10
调用时再接收,那就太棒了。
事实上真的有这种办法,叫做柯里化。
记在脑子里
这次就到这。
接下来的章节。将会讨论柯里化,通用函数(比如map
,filter
,fold
等),参照透明性等。
本文根据@Charles Scalfani的《So You Want to be a Functional Programmer (Part 3)》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-3-1b0fd14eb1a7#.4ua3pz6nd。
如需转载,烦请注明出处:http://www.w3cplus.com/javascript/so-you-want-to-be-a-functional-programmer-part-3.html