Swift函数式编程三(Map、Filter和Reduce)

代码地址

泛型介绍

需求为写一个这样的函数,此函数接收一个参数为整型数组,返回一个一个新数组,新数组各项为原数组对应的数据加一。

func incrementArray(array: [Int]) -> [Int] {
    var result: Array<Int> = []
    for i in array {
        result.append(i + 1)
    }
    
    return result
}

新增需求,再写一个函数,此函数接收一个参数为整型数组,返回一个一个新数组,新数组各项为原数组对应的数据两倍。

func doubleArray(array: [Int]) -> [Int] {
    var result: [Int] = []
    for i in array {
        result.append(i*2)
    }
    
    return result;
}

至此,发现这两个函数有大量的代码相同,能不能写一个更通用的函数?新增一个参数接收一个函数,这个参数根据各个数组项计算新的值。

func computeIntArray(array: [Int], transform: (Int) -> Int) -> [Int] {
    var result: [Int] = []
    for i in array {
        result.append(transform(i))
    }
    
    return result
}

这样就可以简化一点incrementArray、doubleArray这两个函数:

func incrementArray1(array: [Int]) -> [Int] {
    return computeIntArray(array: array, transform: { x in x + 1 })
}
func doubleArray1(array: [Int]) -> [Int] {
    return computeIntArray(array: array, transform: { x in x*2 })
}

代码任然不够灵活,如果需要得到一个布尔型数组,用于表示对应的数字是否为偶数。

func isEvenArray(array: [Int] -> [Bool]) {
    return computeIntArray(array: array, transform: { x in x%2 == 0 })
}

不幸的是上面这段代码无法使用computeIntArray函数,因为类型错误。

于是可以定义一个新的函数接受一个Int -> Bool类型的函数作为参数。

但是这个方案并不好,如果还要计算String类型,还得定义一个高阶函数来接受一个Int -> String类型的函数作为参数。

幸运的是泛型可以解决这个问题。相同的代码可以适用任何类型,写一个适用于每种可能类型的泛型函数:

func genericComputeArray<T>(array: [Int], transform: (Int) -> T) -> [T] {
    var result: [T] = []
    for i in array {
        result.append(transform(i))
    }
    
    return result
}

可以进一步一般化这个函数,没有理由仅能对[Int]型的输入数组进行处理,可以将数组类型也进行抽象:

func map<Element, T>(array: [Element], transform: (Element) -> T) -> [T] {
    var result: [T] = []
    for i in array {
        result.append(transform(i))
    }
    
    return result
}

对于这个map函数在两个维度是通用的,任何类型的数组和transform函数。

按照Swift的惯例将map函数定义为Array的扩展会比定义为顶层函数更合适:

extension Array {
    func map<T>(transform: (Element) -> T) -> [T] {
        var result: [T] = []
        for i in self {
            result.append(transform(i))
        }
        
        return result
    }
}

Element源于Swift的Array中对Element所进行的泛型定义。

map函数已经是Swift标准库中的一部分(基于SequenceType协议被定义)

顶层函数和扩展

在一开始将创建map函数时,为了简便起见,选择了顶层函数的版本。不过最终将map的泛型版本定义为Array的扩展,这和Swift标准库的实现十分相似。

随着协议扩展(protocol extensions),开发者有了强有力的工具来定义扩展–不仅可以在Array这样的具体类型上上进行定义,还可以在SequenceType这样的协议上定义扩展。

把处理确定类型的函数,定义为该类型的扩展。这样的优点是:

  • 自动补全更完善
  • 命名更少
  • 代码结构更清晰

Filter

filter函数像之前定义的map函数一样,接收一个函数作为参数,这个参数类型是(Elemeng) -> Bool–对于数组中的所有元素,此函数判定它是否被包含在结果中:

extension Array {
    func filter(includeElement: (Element) -> BooleanLiteralType) -> [Element] {
        var result: [Element] = []
        for i in self {
            if (includeElement(i)) { result.append(i) }
        }
        
        return result
    }
}

Swift标准库中的数组类型已经定义好了filter函数。

有没有更通用的函数,可以用来定义map,也可以定义filter?

Reduce

reduce函数将变量初始化为某个值,然后对数组的每一项进行遍历,以某种方式更新结果。

extension Array {
    func reduce<T>(initial: T, combine: (T, Element) -> T) -> T {
        var result: T = initial
        for i in self {
            result = combine(result, i)
        }
        
        return result
    }
}

reduce函数的泛型体现在两个方面:

  • 对于任意类型的数组,它会计算一个T类型的返回值。
  • 需要一个T类型的初始值,以及一个用于更新for循环中变量值的函数combine: (T, Elemeng) -> T。

使用reduce定义函数。除了使用闭包,也可以使用操作符作为最后一个参数,使代码更简短。

func sumUsingReduce(xs: [Int]) -> Int {
    return xs.reduce(initial: 0, combine: { result, i in result + i })
}
func productUsingReduce(xs: [Int]) -> Int {
    return xs.reduce(initial: 1, combine: *)
}
func concatUsingReduce(xs: [String]) -> String {
    return xs.reduce(initial: "", combine: +)
}

甚至可以使用reduce重新定义map、filter。

extension Array {
    func mapUsingReduce<T>(transform: (Element) -> T) -> [T] {
        return self.reduce(initial: [T](), combine: { result, i in result + [transform(i)] })
    }
    func filterUsingReduce(includeElement: (Element) -> Bool) -> [Element] {
        return self.reduce(initial: [Element](), combine: { result, i in includeElement(i) ? result + [i] : result })
    }
}

能够用reduce表示这些函数,说明了reduce能够通过通用的方法来体现一种常见的编程模式:遍历数组并计算结果。

注意:
使用reduce来定义一切非常的简便,但是实践中这往往不是一个好主意。原因是,代码在最终的运行期间大量复制生成的数组,换句话说,它不得不反复的分配内存释放内存以及复制内存中的内容。像之前那样用一个可变数组定义map显然效率更高。理论上,编译器可以优化代码使其速度和可变数组一样快,但是Swift2.0并没有做优化。

实际运用

假设有一个City结构体,由城市名称和人口(万)组成。并定义了一些城市示例。

struct City {
    let name: String
    let population: Int
}
let beijing = City(name: "北京", population: 4000)
let shanghai = City(name: "上海", population: 3500)
let guangzhou = City(name: "广州", population: 3000)
let shenzhen = City(name: "深圳", population: 2500)

let citys = [beijing, shanghai, guangzhou, shenzhen]

现在赛选出居民数量至少为3000万的城市,并打印一份这些城市名称及人口数的列表。

extension City {
    func cityByScalingPopulation() -> City { return City(name: self.name, population: self.population*10000) }
}

let table = citys.filter(includeElement: { city in city.population >= 3000 }).map(transform: { city in city.cityByScalingPopulation() }).reduce(initial: "\n城市:人口\n", combine: { result, city in result + "\n\(city.name):\(city.population)\n"})

泛型和Any类型

Any类型和泛型都能定义接收不同类型的参数的函数。然而两者之间的重要区别是:

  • 泛型可以用于定义灵活的函数,类型检查仍由编译器负责。
  • Any类型直接避开了Swift的类型系统(尽可能避免使用)。

用泛型和Any类型分别构造一个函数,除了返回它的参数,其他什么也不做。

func noOp<T>(x: T) -> T { return x }
func noOpAny(x: Any) -> Any { return x }

noOp和noOpAny函数都接收任意参数,关键区别在于返回值,noOp的返回值类型必须跟参数一样,而noOpAny的返回值可以为任何类型,甚至可以和参数的类型不同。如下函数noOpWrong会导致类型错误:

func noOpWrong<T>(x: T) -> T { return 0 }
func noOpAnyWrong(x: Any) -> Any { return 0 }

泛型函数的类型十分丰富,考虑把上一篇Swift函数式编程二(封装Core Image)中的函数组合运算符>>>定义为泛型版本:

precedencegroup ComposeFunctionPrecedence {
    associativity: left
}
infix operator >>>: ComposeFunctionPrecedence
func >>><A, B, C>(f: @escaping (A) -> B, g: @escaping (B) -> C) -> (A) -> C {
    return { x in g(f(x)) }
}

最后用相同的方式定义一个泛型函数,这个函数的作用是将接受两个参数作为输入的函数进行柯里化处理,生成相应的柯里化版本:

func curry<A, B, C>(f: @escaping (A, B) -> C) -> (A) -> (B) -> C {
    return { x in return { y in f(x, y) } }
}

使用泛型,能够在不牺牲类型安全的情况下写出灵活的函数;而使用Any类型,则无法办到。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值