java 高阶函数_函数式编程(Java描述)——使用高阶函数操作列表

在讨论高阶函数前,我希望你们能明白两个离散数学上的概念,单位元和零元

单位元:

又称为么元,它是一个与集合里与二元操作有关的特殊元素。假设集合U上存在一种二元运算 * ,我们称元素t是运算 * 的单位元,当且仅当对任意r属于集合U,存在t * r = r* t =r。当t * r = r时,称左单位元,r * t = r时为右单位元。

例如8c59c5c94e915b0757b8057cf02b38a5.png

零元

也是一个与集合里与二元操作有关的特殊元素。假设集合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方法。c190b0659ab25de2c7d39a146e1c545d.png

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类。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值