目录

前言

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目前来说相对复杂些,毕竟年轻。

参考

Python学习系列之Map,Reduce和 Filter

Go编程模式:Map-Reduce

图解 Map、Reduce 和 Filter


@tsingchan