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类型,则无法办到。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345