欢迎浏览我的博客 获取更多精彩文章
从命令式编程到函数式编程(二)
在这篇文章中,我们要逐步消灭形式上的for循环
我们先抽象地看一下循环是一个什么东西.
在程度的角度看来,循环就是不断地迭代,其迭代的内容是迭代列表中的元素.
无论是
for(int i = 0;i<N;i++){
//do something...
}
还是
for(String string: ListOfString){
//do something...
}
甚至是while和do-while循环,本质上并没有什么不同,只是在语法上有不同的形式罢了.
但是迭代中,最重要的是,我们要在迭代里面做什么.如果这样看的话,我们只要指定开始和结束的条件,并说明要做的事情,而不是被循环所绑定.
试想一下,我们在迭代中,经常做的事情无非就是以下几种:
- 将每个元素转换成其他元素
- 将多个元素聚合成一个结果
- 根据元素自身的条件删除一些元素
- 根据外部条件删除一些元素
- 根据某些条件对元素进行分组
根据上文,我们可以用一个抽象迭代来实现这些操作
函数式的列表操作
列表映射
所谓映射,就是对列表中的每一个元素进行一定的操作.比如下面的程序,对列表中的每个元素乘1.2倍
List<Double> newList = new ArrayList<>();
for (Integer value : integerList){
newList.add(value*1.2);
}
我们可以把这个操作抽象成一个map操作.
在给出代码之前,我们首先看看在这个迭代中做了什么.首先,创建了一个新的List,然后对已有的List进行迭代,里面的每一个元素进行一个操作后,返回新的值,并加入到新的List中.
我们首先可以用上一篇中给出的Function接口将对元素的操作抽象出来.其次,我们将这个操作拓展成一个方法.于是可以得到下面的map方法
<T, U> List<U> map(List<T> list, Function<T, U> function) {
List<U> newList = new ArrayList<>();
for (T value : list) {
newList.add(function.apply(value));
}
return newList;
}
用法如下:
List<Integer> intList = Arrays.asList(1,2,3,4,5,6,7,8);
Function<Integer,Double> timesFunction = x->x*1.5;
List<Double> doubleList = CollectionUtils.map(intList, timesFunction);
System.out.println(doubleList);//[1.2, 2.4, 3.5999999999999996, 4.8, 6.0, 7.199999999999999, 8.4, 9.6]
创建列表
接下来,定义一些以后会用到的创建列表的方法
/**
* 创建一个空列表
*/
public static <T> List<T> list(){
return Collections.emptyList();
}
/**
* 创建一个包含一个元素的列表
*/
public static <T> List<T> list(T t) {
return Collections.singletonList(t);
}
/**
* 创建一个包含一个集合里的元素的列表
*/
public static <T> List<T> list(List<T> ts) {
return Collections.unmodifiableList(new ArrayList<>(ts));
}
/**
* 创建一个读取变长参数的列表
*/
public static <T> List<T> list(T... a) {
return Collections.unmodifiableList(Arrays.asList(a));
}
操作列表
/**
* 读取列表中第一个数据
*/
public static <T> T head(List<T> list) {
if (list.size() == 0) {
throw new IllegalStateException("list is empty");
}
return list.get(0);
}
/**
* 读取列表中除第一个数据外的其他数据
**/
public static <T> List<T> tail(List<T> list) {
if (list.size() == 0) {
throw new IllegalStateException("list is empty");
}
List<T> workList = new ArrayList<>(list);
workList.remove(0);
return Collections.unmodifiableList(workList);
}
/**
* 添加元素
*/
public static <T> List<T> append(List<T> list, T t) {
List<T> ts = new ArrayList<>(list);
ts.add(t);
return Collections.unmodifiableList(ts);
}
化简与折叠列表
列表的折叠实际上是一个聚合操作,通过一定的应用将列表转换为一个单值.最简单的折叠就是对列表中的元素求和.
在列表的折叠上,我们有两个方向,从前到后和从后到前.
- 如果操作可以交换,则两种折叠的方式等价
- 如果操作不可交换,则两种折叠方式会有不同结果
折叠操作需要一个起始值,一个累加器和一个可迭代的列表
我们先创建一个对列表进行左折叠的方法
/**
* 对列表进行折叠
*/
public static <T,U> U fold(List<T> ls, U identity, Function<U, Function<T, U>> f) {
U result = identity;
for (T t : ls) {
result = f.apply(result).apply(t);
}
return result;
}
从递归的角度来看,左折叠是非递归(反递归)执行的,而右折叠是递归执行的,我们需要反序来处理列表
/**
* 对列表进行右折叠,这是一个递归执行的版本
*/
public static <T, U> U foldRight(List<T> ls, U identity, Function<T, Function<U, U>> f) {
return ls.isEmpty()
? identity
: f.apply(head(ls)).apply(foldRight(tail(ls),identity,f));
}
反转列表
/**
* 反转列表
*/
public static <T> List<T> reverse(List<T> list) {
return foldLeft(list, list(),
x -> y -> foldLeft(x, list(y),
a -> b -> append(a, b)));
}
或许这样的反转列表的方法会有点难以理解,那么,我们可以将其拆解
假设我们定义了这么一个函数,可以将一个元素添加到列表的头部
public static <T> List<T> prepend(T t,List<T> list){
return foldLeft(list,list(t),a->b->append(a,b));
}
这个函数做了这么一件事,它将要添加的元素作为一个列表,放到了foldLeft函数的起始值位置,并且其应用是将列表中的元素不断添加,那么它的结果就是起始值中的列表中的元素作为结果列表中的第一个元素,其他元素一直在后面.
想想我们在反转列表时要做什么呢,我们可以想象,有一个空列表,将要反转列表中的元素从前往后遍历,逐个添加到起始值列表的头部
就像这样
public static <T> List<T> reverse(List<T> list){
return foldLeft(list,list(),a->b->prepend(b,a));
}
那么,我们只要把prepend函数中的值,移到reverse中,就成为上面所示的代码了
重写map
现在我们重新看一下map操作
public static <T, U> List<U> map(List<T> list, Function<T, U> function) {
List<U> newList = new ArrayList<>();
for (T value : list) {
newList.add(function.apply(value));
}
return newList;
}
会发现他做的就是两件事,一是从空值列表开始,折叠列表,将每个元素添加到新的列表中.二是在添加元素前,对这个元素应用函数的应用.
那么我们就可以将其写成foldLeft的版本了
public static <T, U> List<U> map(List<T> list, Function<T, U> function) {
return foldLeft(list, list(), x -> y -> append(x, function.apply(y)));
}
这就是函数式编程的美感,可以用行为而不是命令来进行编程.
复合映射
在从命令式编程到函数式编程(一)中,我们演示了如何对函数进行复合,所谓函数,就是对单个元素进行应用,使之成为另一个单个元素,那么从这个角度看来,映射(map)与函数(function)的作用是相似的,只是他们的维度不一样而已.
假设我们有两个函数
public Function<Integer,Integer> func1=...;
public Function<Integer,Integer> func2=...;
我们如果要复合映射,实际上就是对传入映射中的函数进行复合,顺序也与上一篇相同
map(list,func1.andThen(func2));
map(list,func2.compose(func1));
这两种方式都定义了一个先执行func1再执行func2的映射
对列表元素进行作用
在从命令式编程到函数式编程(一)中,我们使用Effect接口对元素进行作用,如果要对列表进行作用,我们就可以定义一个foreach函数
/**
* 对集合中的每一个元素应用作用
*/
public static <T> void forEach(Collection<T> ts, Effect<T> e){
for (T t : ts) {
e.apply(t);
}
}
构建反递归列表
什么是反递归列表呢,它是从[0,limit)的一个整形列表.比如
for(int i =0;i<limit;i++){
//do something
}
这是两个抽象的操作进行复合,一个是反递归列表,一个是处理.
在函数式编程中,我们可以构建一个列表,然后将这个操作映射到跟//do something有关的函数上
我们首先创建一个构建列表的函数
/**
* 构建列表,范围是[start,end)
*/
public static List<Integer> range(int start, int end) {
List<Integer> list = new ArrayList<>();
int temp = start;
while (temp < end) {
list = append(list, temp);
temp++;
}
return list;
}
那么,我们就可以使用map函数来进行操作了