Microbubu的迷你实验室

集合的Map与Reduce操作

字数统计: 1.1k阅读时长: 4 min
2018/12/12 Share

最近有点闲暇时间,就看了看Kotlin的语法,发现一些非常有意思的东西,比如对于集合的Map与Reduce操作,为此很值得做点读书笔记。下面总结一下Kotlin中的MapReduce操作及其在C#中的实现。

现在有一个很简单的需求,将1-100这100个数中3的倍数找出来,并且用”3,6,9…”这样每两个数字中间有一个逗号间隔的形式输出,那么采用原始方法也不复杂:

1
2
3
4
5
6
7
8
9
10
var list = Enumerable.Range(1, 100).ToList();
var result = string.Empty;
foreach(var v in list)
{
if (v % 3 != 0) continue;
result += $"{v},";
}
if (!string.IsNullOrEmpty(result))
result = result.TrimEnd(',');//去掉最后多出来的一个逗号
Console.WriteLine(result);

但是使用MapReduce方法的话就更简单了,看看Kotlin中是怎么做的:

1
2
3
4
5
6
val myList = 1..100
val reduceStr = myList
.filter { it % 3 == 0 } // filter
.map { it.toString() } // map
.reduce { a, b -> "$a,$b" } // reduce
println(reduceStr)

看起来非常简单,其实这个东西在C#中使用Linq同样可以做到类似简洁的函数式编程,下面就使用C#写一下(需要引用System.Linq名称空间):

1
2
3
4
5
6
var list = Enumerable.Range(1, 100).ToList();
var reduceStr = list
.FindAll(v => v % 3 == 0) // 筛选,同filter
.Select(v => v.ToString()) // 选择,同map
.Aggregate((v1, v2) => $"{v1},{v2}"); // 聚合,同reduce
Console.WriteLine(reduceStr);

这样,看起来C#和Kotlin的语法基本一致,只是使用的方法名称不同,如果我们追求完全一样的方法名称,可以对C#现存的类进行扩展:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static class Extension
{
public static List<T> Filter<T>(
this List<T> list, Predicate<T> match)
=> list.FindAll(match);

public static IEnumerable<TResult> Map<T, TResult>(
this IEnumerable<T> list, Func<T, TResult> selector)
=> list.Select(selector);

public static T Reduce<T>(
this IEnumerable<T> list, Func<T, T, T> func)
=> list.Aggregate(func);
}

这里的扩展只是将原来的FindAll/Select/Aggregate方法换了个名称,扩展之后就可以在C#代码中写出和kotlin完全一致的代码了。

另外,从这里也可以看出kotlin从C#中借鉴了很多东西,比如这里的lambda及Linq的语法,除此之外还有扩展方法和可空类型等,例如C#中可以使用int?表明这是一个可以为空的整型变量,而Kotlin中也有相同的Int?类型,当然,两者从int/Int这种值类型转化为int?/Int?这种引用类型都需要进行装箱操作。

其实,这种映射聚合的思想可以解决很多问题,一般都是采用和前面例子中类似的三步走战略:筛选—>映射—>聚合,便可以使用很少的代码得到我们想要的结果,比如下面的例子:

  1. 求首项为1,公差为2的等差数列的前20项的乘积:
1
2
3
var list = Enumerable.Range(1, 20);
var result = list.Map(v => 1 + 2 * (v - 1)).Reduce((v1, v2) => v1 * v2);
Console.WriteLine(result);
  1. 词频统计:
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
var texts = new List<string>()
{
"Good Morning",
"Good Evening",
"Have a nice day",
"Have a nice Morning"
};

var wordCountDict = texts
.Map(v =>
{
var dict = new Dictionary<string, int>();
foreach (var item in v.Split())
{
if (dict.ContainsKey(item))
dict[item]++;
else dict.Add(item, 1);
}
return dict;
})
.Reduce((v1, v2) =>
{
foreach (var key in v2.Keys)
{
if (v1.ContainsKey(key))
v1[key] += v2[key];
else v1.Add(key, v2[key]);
}
return v1;
});

wordCountDict.Keys
.ToList()
.ForEach(v => Console.WriteLine($"{v,-10}:{wordCountDict[v]}"));

输出结果:

1
2
3
4
5
6
7
Good      :2
Morning :2
Evening :1
Have :2
a :2
nice :2
day :1

例2只是作为一个说明的例子,但是这样的解决方案付出的代价是比较高的,因为需要在Map的过程中实例化很多Dictionary的实例,如果数据比较多的话对内存的压力较高。

MapReduce在分布式计算中起着举足轻重的作用,一般而言,分布式计算就是采用这种先分后合的思想。将一项很复杂的任务交给分布式计算机去处理,其做法是先将整个大任务分割成很多小任务,让每一台子计算机处理其中一个小任务,这便是Map的过程,然后将Map形成的中间结果再汇总起来,得到最后需要的结果,这便是Reduce的过程。

CATALOG