Map-Reduce-Filter控制逻辑
目录
前言
Map/Reduce/Filter是一种控制逻辑。
我们先看一张图:
通俗点说:
- map是用同样方法把所有数据都改成别的数据(
映射
),比如把列表/数组/键值对的每个数都换成其平方 - reduce是用某种方法依次把所有数据丢进去最后得到一个结果(
化简
/归约
),比如计算一个列表/数组/键值对所有数的和的过程 - filter是筛选出其中满足某个条件的那些数据(
过滤
)
php数组版
对于php开发者可能比较少去关心map-reduce的实现方式,因为php已经内置了数组相关的map、reduce、filter的函数了,不需要开发者再次实现,那我们看下如何使用这些函数:
-
array_map
为数组的每个元素应用回调函数 ,并返回新数组
array_map( callable $callback, array $array, array ...$arrays) : array
array_map():返回数组,是为 array 每个元素应用 callback函数之后的数组。 array_map() 返回一个 array,数组内容为 array1 的元素按索引顺序为参数调用 callback 后的结果(有更多数组时,还会传入 arrays 的元素)。 callback 函数形参的数量必须匹配 array_map() 实参中数组的数量。
注意:array_map的第一个参数是callback回调函数,和array_reduce、array_filter有区别,因为array_map支持多个数组的回调处理
function cube($n) { return ($n * $n * $n); } $a = [1, 2, 3, 4, 5]; $b = array_map('cube', $a); print_r($b); //匿名函数方式 //$b = array_map(function($n){return ($n*$n*$n);},[1,2,3,4,5]); //print($b);
Array ( [0] => 1 [1] => 8 [2] => 27 [3] => 64 [4] => 125 )
php还提供了array_walk函数,这个函数和array_map很像,但array_walk不返回新数组,而是返回true或false,相当于在array_walk中遍历数组,并处理逻辑甚至输出。
array_walk( array &$array, callable $callback[, mixed $userdata = NULL] ) : bool array_walk_recursive( array &$array, callable $callback[, mixed $userdata = NULL] ) : bool
-
array_reduce
用回调函数迭代地将数组简化为单一的值,最后返回一个值
array_reduce( array $array, callable $callback[, mixed $initial = NULL] ) : mixed
array_reduce() 将回调函数 callback 迭代地作用到 array 数组中的每一个单元中,从而将数组简化为单一的值。
//$carray 是上一次迭代后的值,如果是第一次,默认是array_reduce的第三个参数initial的值 function sum($carry, $item) { //if(){} $carry += $item; return $carry; } function len($carry,$item){ $carry += strlen($item); return $carry } function product($carry, $item) { $carry *= $item; return $carry; } $a = [1, 2, 3, 4, 5]; $x = ["apple","banana","city","deer"]; var_dump(array_reduce($a, "sum")); // int(15) var_dump(array_reduce($a, "product", 10)); // int(1200), 因为给定了第三个参数initial参数10: 10*1*2*3*4*5 var_dump(array_reduce($x,"len"));//int(19)
php还提供了array_sum函数,用于规约数组中的所有值,即对数组中所有值求和,这个函数其实就是相当于array_reduce调用了一个计算所有值之和的sum回调函数。
-
array_filter
用回调函数过滤数组中的单元,最后返回一个新数组
array_filter( array $array[, callable $callback[, int $flag = 0]] ) : array
依次将 array 数组中的每个值传递到 callback 函数。如果 callback 函数返回 true,则 array 数组的当前值会被包含在返回的结果数组中。数组的键名保留不变。
function odd($var) { //如果var为奇数则将返回true return($var & 1); } $array1 = ["a"=>1, "b"=>2, "c"=>3, "d"=>4, "e"=>5]; echo "Odd :\n"; print_r(array_filter($array1, "odd"));
Odd : Array ( [a] => 1 [c] => 3 [e] => 5 )
再看参数flag的作用,默认回调函数的参数为数组的值,但我们可以通过flag参数来确定回调函数的入参是什么:
$arr = ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]; var_dump(array_filter($arr, function($k) { return $k == 'b'; }, ARRAY_FILTER_USE_KEY));//指定callback的入参$k是数组的key var_dump(array_filter($arr, function($v, $k) { return $k == 'b' || $v == 4; }, ARRAY_FILTER_USE_BOTH));//指定callback的入参有两个,$k是数组的key,$v是数组元素的值
这三个是数组很常用的关于map、reduce、filter的函数,在大数据处理上我们经常看到Map-Reduce这个专业术语,其实一开始也是函数式编程里的思想,map就是映射,reduce是归一化(或规约)。在很多场合中会使用到,php目前本质还是主要在处理字符串、数组,尽可能的发挥我们脑海里的各种想法算法来折腾字符串和数组,以满足我们的需求。
golang版
golang并没有内置相应的函数,但这三个函数都是我们函数编程模式下常用的三个操作:map、reduce、filter,需要我们手动实现:
-
map
这里我们编写两个函数IntArrayMap和StrArrayMap,第一个用于处理int16的数组数据映射成一个新的int16的数组(每个数值是原来数值的立方数值),第二个用于处理字符串数组,将数组中所有字母转换成大写字母,也就是将字符串数组映射成新的全部大写的字符串数组。
package main import "fmt" func IntArrayMap(arr []int16,fn func(num int16) int16) []int16 { var newArr = []int16{} for _,one := range arr{ newArr = append(newArr,fn(one)) } return newArr } func main(){ var list = []int16{1,2,3,4,5} res := IntArrayMap(list,func(num int16) int16 { return num*num*num }) fmt.Printf("%v\n",res) }
demo>go run hello.go [1 8 27 64 125]
package main import ( "fmt" "strings" ) func StrArrayMap(arr []string,fn func(str string) string) []string { var newArr = []string{} for _,one := range arr{ newArr = append(newArr,fn(one)) } return newArr } func main(){ var list = []string{"i","love","china","and","chinese"} res := StrArrayMap(list,func(str string) string { return strings.ToUpper(str) }) fmt.Printf("%v\n",res) }
demo>go run hello.go [I LOVE CHINA AND CHINESE]
golang这个map的函数编程是类似的,只是受困于强类型与泛型的缘故,不能像php的array_map函数可以自由处理任何类型数据,只需要一个函数就可以满足开发者的需求,当然弊端也就是存在类型错误的异常情况,只能在执行阶段才被发现,而像golang这样的强类型静态语言,在编码阶段或编译期间就会检查这些潜在的异常,不会给与任何这种级别的错误,所以可靠性更高,但代价就是除了业务逻辑编码外,开发者还需要花相对多的时间在控制逻辑编码上。(当然上面的demo代码可以继续优化,这里仅用来展示map的使用与用途)
-
reduce
同样的,reduce,也需要我们手动去实现:
package main import ( "fmt" ) func IntArrayReduce(arr []int,fn func(num int) int ) int { var sum int = 0 for _,one := range arr{ sum += fn(one) } return sum } func main(){ var list = []int{0,1,2,3,4,5,6,7,8,9} res := IntArrayReduce(list,func(num int) int { return num }) fmt.Printf("%v\n",res) }
demo>go run hello.go 45
上面回调的匿名函数是返回原来的数值return num,并没有做任何处理,这里我们可以做一些我们需要的处理,比如过滤指定哪些数值才相加(奇数或偶数或满足条件的数值)
-
filter
package main import ( "fmt" ) func IntArrayFilter(arr []int,fn func(num int) bool ) []int { var newArray = []int{} for _,one := range arr{ if fn(one) { newArray = append(newArray,one) } } return newArray } func main(){ var list = []int{0,1,2,3,4,5,6,7,8,9} res := IntArrayFilter(list,func(num int) bool { if (num & 1) == 1 { return true } return false }) fmt.Printf("%v\n",res) }
demo>go run hello.go [1 3 5 7 9]
python列表版
-
map
列表元素每个值的平方:
def newfunc(a): return a*a x = map(newfunc, (1,2,3,4)) #x is the map object print(x) print(set(x)) # <map object at 0x000001B8654D4DC8> # {16, 1, 4, 9}
还可以使用lambda函数实现匿名函数回调的方式,以下元组的每个值实现加3:
tup= (5, 7, 22, 97, 54, 62, 77, 23, 73, 61) newtuple = tuple(map(lambda x: x+3 , tup)) print(newtuple) #输出:(8, 10, 25, 100, 57, 65, 80, 26, 76, 64)
-
reduce
实现列表的值依次相加求和:
# 注意需要从functools引入reduce from functools import reduce res = reduce(lambda a,b: a+b,[23,21,45,98]) print(res) # 187
-
filter
就一行:返回大于3的值的新列表:
y = filter(lambda x: (x>=3), (1,2,3,4)) print(list(y)) # [3, 4]
控制逻辑与业务逻辑分离golang版本
看到陈皓大佬在分享关于go编程模式的文章,有一篇关于map-reduce,这里借用文章中一个业务示例,一方面进一步理解map-reduce-filter的原理与使用,一方面也进一步清晰控制逻辑与业务逻辑。
-
员工信息
首先,我们一个员工对象,以及一些数据
type Employee struct { Name string Age int Vacation int Salary int } var list = []Employee{ {"Hao", 44, 0, 8000}, {"Bob", 34, 10, 5000}, {"Alice", 23, 5, 9000}, {"Jack", 26, 0, 4000}, {"Tom", 48, 9, 7500}, {"Marry", 29, 0, 6000}, {"Mike", 32, 8, 4000}, }
-
相关的Reduce/Fitler函数
然后,我们有如下的几个函数:
func EmployeeCountIf(list []Employee, fn func(e *Employee) bool) int { count := 0 for i, _ := range list { if fn(&list[i]) { count += 1 } } return count } func EmployeeFilterIn(list []Employee, fn func(e *Employee) bool) []Employee { var newList []Employee for i, _ := range list { if fn(&list[i]) { newList = append(newList, list[i]) } } return newList } func EmployeeSumIf(list []Employee, fn func(e *Employee) int) int { var sum = 0 for i, _ := range list { sum += fn(&list[i]) } return sum }
简单说明一下:
EmployeeConutIf 和 EmployeeSumIf 分别用于统满足某个条件的个数或总数。它们都是Filter + Reduce的语义。
EmployeeFilterIn 就是按某种条件过虑。就是Fitler的语义。
-
各种自定义的统计示例
-
统计有多少员工大于40岁
old := EmployeeCountIf(list, func(e *Employee) bool { return e.Age > 40 }) fmt.Printf("old people: %d\n", old) //old people: 2
-
统计有多少员工薪水大于6000
high_pay := EmployeeCountIf(list, func(e *Employee) bool { return e.Salary >= 6000 }) fmt.Printf("High Salary people: %d\n", high_pay) //High Salary people: 4
-
列出有没有休假的员工
no_vacation := EmployeeFilterIn(list, func(e *Employee) bool { return e.Vacation == 0 }) fmt.Printf("People no vacation: %v\n", no_vacation) //People no vacation: [{Hao 44 0 8000} {Jack 26 0 4000} {Marry 29 0 6000}]
-
统计所有员工的薪资总和
total_pay := EmployeeSumIf(list, func(e *Employee) int { return e.Salary }) fmt.Printf("Total Salary: %d\n", total_pay) //Total Salary: 43500
-
统计30岁以下员工的薪资总和
younger_pay := EmployeeSumIf(list, func(e *Employee) int { if e.Age < 30 { return e.Salary } return 0 })
-
小结
前面我们也提到golang的map-reduce,需要泛型的支持,才能做到有效率的编写控制逻辑,泛型使得map-reduce兼容性更好、健壮性更强、复用性更强。
从代码比较上看,python太优雅简洁了,php也不差,golang目前来说相对复杂些,毕竟年轻。
参考
@tsingchan