在讨论高阶函数前,我希望你们能明白两个离散数学上的概念,单位元和零元
单位元:
又称为么元,它是一个与集合里与二元操作有关的特殊元素。假设集合U上存在一种二元运算 * ,我们称元素t是运算 * 的单位元,当且仅当对任意r属于集合U,存在t * r = r* t =r。当t * r = r时,称左单位元,r * t = r时为右单位元。
例如
零元
也是一个与集合里与二元操作有关的特殊元素。假设集合U上存在一种二元运算 * ,我们称元素t是运算 * 的零元,当且仅当对任意r属于集合U,存在t * r = r* t =t。当t * r = t时,称t为左零元,r * t = t时为右零元。例如,实数上的乘法运算的零元就是0,零元在一些操作中具有短路的作用。
折叠操作
这里有一个保存实数的链表,一般来说,折叠操作的累加器都会使用该操作的单元做初始化值。
如果要计算所有元素之和,一个简单的递归操作可以这样写:
public static Integer sum(List iList) {
return iList.isEmpty() ? 0 : iList.head() + sum(iList.tail());
}
如果我要计算所有元素乘积呢?
public static Integer product(List iList) {
return iList.isEmpty() ? 1 : iList.head() == 0 ? 0 : iList.head() * product(iList.tail());
}//这里使用乘法的零元做短路处理。
仔细观察上述两个函数,我们可以发现它们的基本流程是一样的,这是我们就需要考虑把他们的公共部分抽象提取出来。而这个操作,就是我前面介绍的折叠操作。
现在暂时使用左折叠操作,先看第一个版本:
public abstract U foldLeft(U acc, Function> f);
在Nil类里,此方法仅返回初始累加器acc的值。我们来看Cons类里的实现。
@Override
public U foldLeft(U acc, Function> f) {
return foldLeft(acc, this, f);
}
private U foldLeft(U acc, List list, Function> f) {
return list.isEmpty() ? acc : foldLeft(f.apply(acc).apply(list.head()), list.tail(), f);
}
我们使用TailCall类优化:
@Override
public U foldLeft(U acc, Function> f) {
return foldLeft(acc, this, f).eval();
}
private TailCall foldLeft(U acc, List list, Function> f) { return list.isEmpty() ? ret(acc) : sus(() -> foldLeft(f.apply(acc).apply(list.head()), list.tail(), f));
}
现在,我们可以使用这个折叠操作来实现一开始的那两个方法了:
public static Integer sum(List iList) {
return iList.foldLeft(0, x -> y -> x + y);
}
public static Integer product(List iList) {
return iList.foldLeft(1, x -> y -> x * y);
}
当然了,还可以用它计算列表的长度:
public static Integer length(List list) {
return list.foldLeft(1, x -> y -> x +1);
}
但是这种计算列表的方式很费时,因为你每次获取列表的长度都需要重新计算一篇。我将在以后的文章中介绍在列表中使用记忆化特性。
接下来就是如何使用右折叠了,要定义右折叠,一个现有的方式就是直接通过左折叠实现。我们知道,左折叠与右折叠操作顺序正好相反,因此一个列表的右折叠操作就等价于把它反转后再执行左折叠操作。右折叠定义如下:
public abstract U foldRight(U acc, Function> f);
同样它在Nil类里只返回初始累加器的值,我们只讨论Cons类里的定义
@Override
public U foldRight(U acc, Function> f) {
return reverse().foldLeft(acc, x -> y -> f.apply(y).apply(x));
}
至于里面的f函数为什么要这样写。再左折叠中,函数是U->T->U,而右折叠是T->U->U。函数f是右折叠操作里的函数,第一个apply的值肯定是T类型,而左折叠里提供的T类型在第二位,即它是y,所以要先应用y在应用x。
如果你不喜欢使用左折叠定义右折叠(看完下面这个方法后我相信你会喜欢上的),你也可以从零开始定义右折叠操作。
这是一个递归处理的方式,它适合两个子类
public static U foldRight(U acc, List list, Function> f) {
return list.isEmpty() ? acc : f.apply(list.head()).apply(foldRight(acc, list.tail(), f));
}
我们也可以使用它实现求和操作:
public static Integer sum(List iList) {
return List.foldRight(0, iList, x -> y -> x + y);
}
同样也可以实现求长度的操作:
public int length() {
return foldRight(0, this, x -> y -> y + 1);
}
然而这种递归的右折叠不是尾递归,很容易发生爆栈现象。如果我们改成尾递归的形式
public U foldRight(U acc, Function> f) {
return foldRight0(acc, this.reverse(), f);
}
private U foldRight0(U acc, List list, Function> f) {
return list.isEmpty() ? acc : foldRight0(f.apply(list.head()).apply(acc), list.tail(), f);
}//你自己可以使用TailCall优化
可以看到我们使用了this.reverse()方法反转列表。这是因为想要实现尾递归,就必须从头元素开始处理,这和我们前面将的使用左折叠实现没有太大差别。所以建议你还是直接用左折叠实现右折叠。
折叠操作抽象了递归,我们可以复用它们来实现许多基于递归的操作,其中最重要的就是映射和过滤。不过在此之前,我们先来解决上一章留下的concat操作问题。
我们可以利用右折叠轻松做到
public static List concat(List list1, List list2) {
return list2.foldRight(list1, x -> y -> new Cons<>(x, y));
}
这里使用list1作为累加器。
映射:
一个列表映射后仍然是一个列表,只不过列表中的元素类型可能发生变化,这里有一个包含Interger类型的列表,元素分别为1,2,3,4,5。我们想每一个元素都转为double类型,即1.0,2.0,3.0,4.0,5.0。你可以通过左折叠实现:
public static List intToDouble(List iList) {
return iList.foldLeft(List.list(), x -> y -> x.cons(y * 1.0)).reverse();
}
要注意,因为列表只能操作head,所以如果只使用左折叠的话,最后会变成5.0,4.0,3.0,2.0,1.0。所以需要最后做一次反转。而使用右折叠就不需要显示的反转了。
public static List intToDouble(List iList) {
return iList.foldRight(List.list(), x -> y -> y.cons(x * 1.0));
}
这是因为右折叠的实现里,本身就对列表做了一次反转。
映射操作对放在List类里:
public List map(Function f) {
return foldRight(list(), x -> y -> y.cons(f.apply(x)));
}
还有一种操作叫做flatMap(),它把列表List里的每一个元素t都映射为一个新的列表List,实现也很简单:
public List flatMap(Function> f) {
return foldRight(list(), x -> y -> concat(y, f.apply(x)));
}
注意这里concat函数里参数的顺序,在concat里,第一个参数是作为累加器存在的,而Y也是累加器,所以第一个参数应该是Y。
过滤:
还是使用上面提到的整型列表,加入我们想要找出所有的偶数:
public static List evenNumber(List iList) {
return iList.foldRight(List.list(), x -> y -> x % 2 == 0 ? y.cons(x) : y);
}
可以写个更通用的操作放在List类里。
public List filter(Function f) {
return foldRight(list(), x -> y -> f.apply(x) ? y.cons(x) : y);
}
其他操作
1.求最大最小值—MAX和MIN方法
现在给出静态方法的实现,你也可以将它改为实例方法。但是要注意比较的元素一定要实现了Comparable接口
public static > T max(List list) {
return list.tail().foldRight(list.head(), x -> y -> x.compareTo(y) > 0 ? x : y);
}
在这个方法里,我们将列表的头元素作为累加器,让它和剩余的每个元素作比较。min方法也是同样的实现。
2.平铺展开列表—flatten方法
例如,我这里有一个链表,它包含的元素也是一个链表,我想获取所有子列表的单一列表,就可以使用flatten方法。
public static List flatten(List> list) {
return list.foldRight(List.list(), x -> y -> concat(y, x));
}
实际上,它就是一个flatMap,你直接应用flatMap也可以得到相同的结果:
public static List flatten0(List> list) {
return list.flatMap(x -> x);
}
3.从Java集合里生产列表
我们可以使用普通的Java集合来生产我们的列表:
public static List fromJavaCollection(Collection coll) {
List list = list();
for (T t : coll) {
list = list.cons(t);
}
return list;
}
对列表的介绍暂时就到这了,当然还有许多操作我没有介绍,你可能发现我并没有写如何函数式的处理null,异常等。在接下来的文章中我将带你了解如何处理这些情况,并把它们应用到List上。下一篇,处理可选数据—Option类。