高级函数式技巧

思考并回答以下问题:

到目前为止,您已经对编程的功能风格和优点有了一定的了解它可以带来。您可以将这些技术从今天开始使用,而无需进一步阅读。理想情况下,不过,我激发了您的胃口,以进一步发展它,并了解更多可用于您的功能性技术程序员的工具箱。
在本章中,您将了解函数式编程的一些更高级的方面,这些方面将使您以越来越实用的方式构建PHP代码。本章是您之前的“理论”的最后一部分在本书的下一部分中开始讲一些实际的例子。您将从查看curring开始,扩展了部分函数应用程序的概念,将其分解为可自动化的方式较低种族的版本。接下来,您将了解寓言中的monad,它们可帮助您进行程序流控制和让您处理在现实世界中工作时会遇到的讨厌的副作用。之后,您将了解蹦床,这是一种可控制递归的方法。最后,我会讲关于使用类型声明的严格类型与动态类型的讨论很少,尽管这不是严格的功能概念在某些方面可能有用(在其他方面则没有)。

柯里化函数

柯里化(Currying)指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数为参数的函数。

实例
首先我们定义一个函数:

1
def add(x:Int,y:Int)=x+y

那么我们应用的时候,应该是这样用:add(1,2)

现在我们把这个函数变一下形:

1
def add(x:Int)(y:Int) = x + y

那么我们应用的时候,应该是这样用:add(1)(2),最后结果都一样是3,这种方式(过程)就叫柯里化。

实现过程
add(1)(2) 实际上是依次调用两个普通函数(非柯里化函数),第一次调用使用一个参数 x,返回一个函数类型的值,第二次使用参数y调用这个函数类型的值。

实质上最先演变成这样一个方法:

1
def add(x:Int)=(y:Int)=>x+y

那么这个函数是什么意思呢? 接收一个x为参数,返回一个匿名函数,该匿名函数的定义是:接收一个Int型参数y,函数体为x+y。现在我们来对这个方法进行调用。

1
val result = add(1)

返回一个result,那result的值应该是一个匿名函数:(y:Int)=>1+y

所以为了得到结果,我们继续调用result。

1
val sum = result(2)

最后打印出来的结果就是3。

完整实例
下面是一个完整实例:

1
2
3
4
5
6
7
8
9
10
11
object Test {
def main(args: Array[String]) {
val str1:String = "Hello, "
val str2:String = "Scala!"
println( "str1 + str2 = " + strcat(str1)(str2) )
}

def strcat(s1: String)(s2: String) = {
s1 + s2
}
}

执行以上代码,输出结果为:

$ scalac Test.scala
$ scala Test
str1 + str2 = Hello, Scala!

名义上只接受部分参数,但其实底下偷偷补足其他参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var bind = function(fn, a){
return function(b){
return fn(a, b)
}
}
var mult = function(a, b){
return a*b
}

// 绑定mult的第一个参数a,譬如指定该参数为2
var time2 = bind(mult, 2)

// 调用新函数time2
time(1) // 得2
time(3) // 得6

严谨一点来说,currying之后的函数只接受一个参数,这比一般的部分绑定函数形式上更加精炼。

柯里化通常也称部分求值,其含义是给函数分步传递参数,每次传递参数后部分应用参数,并返回一个更具体的函数接受剩下的参数,这中间可嵌套多层这样的接受部分参数函数,直至返回最后结果。
因此柯里化的过程是逐步传参,逐步缩小函数的适用范围,逐步求解的过程。

柯里化一个求和函数
按照分步求值,我们看一个简单的例子

var concat3Words = function (a, b, c) {
return a+b+c;
};

var concat3WordsCurrying = function(a) {
return function (b) {
return function (c) {
return a+b+c;
};
};
};
console.log(concat3Words(“foo “,”bar “,”baza”)); // foo bar baza
console.log(concat3WordsCurrying(“foo “)); // [Function]
console.log(concat3WordsCurrying(“foo “)(“bar “)(“baza”)); // foo bar baza
可以看到, concat3WordsCurrying(“foo “) 是一个 Function,每次调用都返回一个新的函数,该函数接受另一个调用,然后又返回一个新的函数,直至最后返回结果,分布求解,层层递进。(PS:这里利用了闭包的特点)

那么现在我们更进一步,如果要求可传递的参数不止3个,可以传任意多个参数,当不传参数时输出结果?

首先来个普通的实现:

var add = function(items){
return items.reduce(function(a,b){
return a+b
});
};
console.log(add([1,2,3,4]));
但如果要求把每个数乘以10之后再相加,那么:

1
2
3
4
5
6
7
8
var add = function (items,multi) {
return items.map(function (item) {
return item*multi;
}).reduce(function (a, b) {
return a + b
});
};
console.log(add([1, 2, 3, 4],10));

好在有 map 和 reduce 函数,假如按照这个模式,现在要把每项加1,再汇总,那么我们需要更换map中的函数。

下面看一下柯里化实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var adder = function () {
var _args = [];
return function () {
if (arguments.length === 0) {
return _args.reduce(function (a, b) {
return a + b;
});
}
[].push.apply(_args, [].slice.call(arguments));
return arguments.callee;
}
};
var sum = adder();

console.log(sum); // Function

sum(100,200)(300); // 调用形式灵活,一次调用可输入一个或者多个参数,并且支持链式调用
sum(400);
console.log(sum()); // 1000 (加总计算)
`

上面 adder是柯里化了的函数,它返回一个新的函数,新的函数接收可分批次接受新的参数,延迟到最后一次计算。

通用的柯里化函数
更典型的柯里化会把最后一次的计算封装进一个函数中,再把这个函数作为参数传入柯里化函数,这样即清晰,又灵活。
例如 每项乘以10, 我们可以把处理函数作为参数传入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var currying = function (fn) {
var _args = [];
return function () {
if (arguments.length === 0) {
return fn.apply(this, _args);
}
Array.prototype.push.apply(_args, [].slice.call(arguments));
return arguments.callee;
}
};

var multi=function () {
var total = 0;
for (var i = 0, c; c = arguments[i++];) {
total += c;
}
return total;
};

var sum = currying(multi);

sum(100,200)(300);
sum(400);
console.log(sum()); // 1000 (空白调用时才真正计算)

这样 sum = currying(multi),调用非常清晰,使用效果也堪称绚丽,例如要累加多个值,可以把多个值作为做个参数 sum(1,2,3),也可以支持链式的调用,sum(1)(2)(3)

柯里化的基础
上面的代码其实是一个高阶函数(high-order function), 高阶函数是指操作函数的函数,它接收一个或者多个函数作为参数,并返回一个新函数。此外,还依赖与闭包的特性,来保存中间过程中输入的参数。即:

函数可以作为参数传递
函数能够作为函数的返回值
闭包
柯里化的作用
延迟计算。上面的例子已经比较好低说明了。
参数复用。当在多次调用同一个函数,并且传递的参数绝大多数是相同的,那么该函数可能是一个很好的柯里化候选。
动态创建函数。这可以是在部分计算出结果后,在此基础上动态生成新的函数处理后面的业务,这样省略了重复计算。或者可以通过将要传入调用函数的参数子集,部分应用到函数中,从而动态创造出一个新函数,这个新函数保存了重复传入的参数(以后不必每次都传)。例如,事件浏览器添加事件的辅助方法:

1
2
3
4
5
6
7
8
9
10
11
var addEvent = function(el, type, fn, capture) {
if (window.addEventListener) {
el.addEventListener(type, function(e) {
fn.call(el, e);
}, capture);
} else if (window.attachEvent) {
el.attachEvent("on" + type, function(e) {
fn.call(el, e);
});
}
};

每次添加事件处理都要执行一遍 if…else…,其实在一个浏览器中只要一次判定就可以了,把根据一次判定之后的结果动态生成新的函数,以后就不必重新计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var addEvent = (function(){
if (window.addEventListener) {
return function(el, sType, fn, capture) {
el.addEventListener(sType, function(e) {
fn.call(el, e);
}, (capture));
};
} else if (window.attachEvent) {
return function(el, sType, fn, capture) {
el.attachEvent("on" + sType, function(e) {
fn.call(el, e);
});
};
}
})();

这个例子,第一次 if…else… 判断之后,完成了部分计算,动态创建新的函数来处理后面传入的参数,这是一个典型的柯里化。

您在上一章中了解了部分函数的优点,并且我提到了一种用于自动分解功能。分解是分解多arar函数的行为通过固定一个或多个参数的值,将其转换为具有较小签名的函数。方法您要查看的自动分解称为currying,并以Haskell Curry(一个男人其名称(字面上)是整个函数式编程!
咖喱确实与部分功能的应用密切相关,乍看之下它是咖喱功能看起来很像您创建的部分函数生成器。但是,有一些微妙但重要的差异。就是说,部分功能应用程序只是一种currying(或者相反),取决于您与谁交谈),因此每种方法的好处是相似的。您选择使用哪个取决于什么根据您的情况为您工作。
在部分函数生成器中,您获取了一个函数,以及一个绑定到第一个参数的值,然后返回了一个签名短了一个参数的函数。在currying中,您可以使其更加灵活通过获取一个函数和一个或多个参数的列表并将所有给定的参数绑定到新函数回到。到目前为止,咖喱相似(如果更笼统)。要获得功能的实际结果,您需要到达已绑定和/或传递函数所有参数的位置,函数将然后执行并返回一个值。

生成器和currying函数返回的部分函数(在两种情况下均为闭包)是把握两者之间差异的关键。使用您看过的简单的部分生成器,返回的函数是签名减少的函数(即,叫它)。如果您想进一步减少它来创建另一个局部函数,则可以调用局部函数在返回的闭包上再次生成函数。相反,闭包是由一个循环例程返回的是一项独立功能,可以自动自动进行进一步处理。例如,如果您有一个具有五个参数的函数,并且通过固定两个参数来进行咖喱处理,则将得到一个接受三个闭包
参数。如果然后再用一个参数调用该闭包,则不要执行不完整的闭包参数集(如前面显示的部分函数所示),它将自动咖喱自身并返回另一个接受两个参数的闭包(如果使用,则再次具有进一步咖喱自己的能力另一个参数)。将此与生成器的部分功能进行对比;如果您提供一个接受三个部分函数的参数,它将尝试使用简化的参数集执行,通常会导致错误。

与往常一样,用一个例子可能会更清楚。编写适当形式的currying函数并非易事,您将使用Matteo Giachino编写的名为php-curry的库来帮助您。这是可用的在GitHub上的[https://github.com/matteosister/php-curry]上,可以通过Composer或如清单4-1和清单4-2所示,直接包含它。

Listing 4-1. currying.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php
include('Curry/Placeholder.php');
include('Curry/functions.php');
use Cypress\Curry as C;
# Let's make a function place an order with our chef
# for some delicious curry (the food, not the function)
$make_a_curry = function($meat, $chili, $amount, $extras, $where) {
return [
"Meat type"=>$meat,
"Chili hotness"=>$chili,
"Quantity to make"=>$amount,
"Extras"=>$extras,
"Eat in or take out"=>$where
];
};
# We think that everyone will want a mild Rogan Josh, so
# let's curry the function with the first two parameters
$rogan_josh = C\curry($make_a_curry, 'Lamb','mild');
# $rogan_josh is now a closure that will continue to
# curry with the arguments we give it
$dishes = $rogan_josh("2 portions");
# likewise $dishes is now a closure that will continue
# to curry
$meal = $dishes('Naan bread');
# and so on for meal. However, we only have 1 parameter
# which we've not used, $where, and so when we add
# that, rather than returning another closure, $meal
# will execute and return the result of $make_a_curry
$order = $meal('Eat in');
print_r( $order );
# To show that our original function remains unmutated, when
# we realize that actually people only want 1 portion of curry
# at a time, with popadoms, and they want to eat it at home, we
# can curry it again. This time, the parameters we want to bind
# are at the end, so we use curry_right.
$meal_type = C\curry_right($make_a_curry, 'Take out', 'Poppadoms', '1 portion');
$madrass = $meal_type('hot', 'Chicken');
print_r( $madrass );
# We could curry the function with all of the parameters
# provided, this creates a parameter-less closure but doesn't
# execute it until we explicitly do so.
$korma = C\curry($make_a_curry,
'Chicken', 'Extra mild', 'Bucket full', 'Diet cola', 'Eat in');
print_r($korma());

Listing 4-2. currying-output.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Array
(
[Meat type] => Lamb
[Chili hotness] => mild
[Quantity to make] => 2 portions
[Extras] => Naan bread
[Eat in or take out] => Eat in
)
Array
(
[Meat type] => Chicken
[Chili hotness] => hot
[Quantity to make] => 1 portion
[Extras] => Poppadoms
[Eat in or take out] => Take out
)
Array
(
[Meat type] => Chicken
[Chili hotness] => Extra mild
[Quantity to make] => Bucket full
[Extras] => Diet cola
[Eat in or take out] => Eat in
)
0%