【函数式 Swift】封装 Core Image

前一章《函数式思想》中,咱们学习了函数式编程思想,即,经过构建一系列简单、实用的函数,再“装配”起来解决实际问题。本章借助一个封装 Core Image 的案例进一步练习函数式思想的应用。javascript

本章关键词

请带着如下关键词阅读本文:java

  • 高阶函数
  • 柯里化

案例:封装 Core Image

本章案例是对 Core Image 中的 CIFilter 进行再次封装。开始看到这一章时,我就有疑问:为何会选择这个不是很经常使用,但已经功能很是全面的 API 做为封装对象呢?后来想一想,封装这种看上去没什么可封装的 API 恰好可让咱们体会到函数式思想的内涵,咱们甚至能够不去关注函数的具体实现(下文中,我将会省略部分函数内部的具体实现,仅关注如何封装高质量函数)。git

若是你不了解 CIFilter 的用途,能够先来看看下面这段代码和效果:github

let image = UIImage(named: "graynoel")

guard image != nil else { // 1
    fatalError()
}
let originImage = CIImage(image: image!)
let filter = CIFilter(name: "CIGaussianBlur") // 2

guard filter != nil else { // 3
    fatalError()
}
filter!.setValue(originImage, forKey: kCIInputImageKey) // 4
filter!.setValue(5.0, forKey: kCIInputRadiusKey) // 5

guard filter!.outputImage != nil else { // 6
    fatalError()
}
let outputImage = UIImage(ciImage: filter!.outputImage!)复制代码

代码逻辑比较简单:读入一个 UIImage 对象,转换为 CIImage 实例,而后经过 Core Image 中的 CIFilter API 对其进行“高斯模糊”处理。咱们不用去关心实现逻辑,只看看这段代码自己的问题:编程

  1. 防护型代码不少:注释 一、三、6 处;
  2. 使用字符串配置滤镜类型:注释 2 处;
  3. 使用键值方式配置滤镜参数:注释 四、5 处。

这些问题当咱们仅处理少许图片时彷佛尚可接受,但大量使用时代码就变得复杂了。swift

如何优化呢?很容易想到的解决办法就是把须要的滤镜变化封装成函数,一个可能的函数封装方式以下:api

func blur(originImage: CIImage, radius: Double) -> CIImage {
    let outputImage: CIImage
    ...
    return outputImage
}复制代码

这样作能够达到效果,但是并无使用函数式思想,那函数式思想指导下的解决方案是什么样呢?ide

借助前一章《函数式思想》的结论,首先应该选择一个合适的类型,而合适的类型直接取决于所要实现功能的输入和输出,很明显,输入、输出都是 CIImage,那么,咱们能够获得如下函数式定义:函数式编程

typealias Filter = (CIImage) -> CIImage复制代码

若是您读过前一章的内容,看到这句定义后,应该会有一种豁然开朗的感受。此时,咱们的目标发生了变化,再也不是为了实现某一些具体的滤镜方法,而是构建滤镜相关的“工具库”,为一切高阶函数封装提供可能。函数

有了这个认识,咱们从新定义 blur 函数,并与上文中的函数进行对比:

// after
func blur(radius: Double) -> Filter {
    return { image in
        let outputImage: CIImage
        ...
        return outputImage
    }
}

// before
func blur(originImage: CIImage, radius: Double) -> CIImage {
    let outputImage: CIImage
    ...
    return outputImage
}复制代码

基于这个思路,咱们能够封装不少相似的函数:

func colorGenerator(color: UIColor) -> Filter {
    return { _ in
        let c = CIColor(color: color)
        let parameters = [kCIInputColorKey: c]
        guard let filter = CIFilter(name: "CIConstantColorGenerator", withInputParameters: parameters) else {
            fatalError()
        }
        guard let outputImage = filter.outputImage else { fatalError() }
        return outputImage
    }
}


func compositeSourceOver(overlay: CIImage) -> Filter {
    return { image in
        let parameters = [
            kCIInputBackgroundImageKey: image,
            kCIInputImageKey: overlay
        ]
        guard let filter = CIFilter(name: "CISourceOverCompositing", withInputParameters: parameters) else {
            fatalError()
        }
        guard let outputImage = filter.outputImage else { fatalError() }
        let cropRect = image.extent
        return outputImage.cropping(to: cropRect)
    }
}复制代码

获得这个小型工具库以后,咱们能够很是便捷的“装配”出复杂的滤镜效果函数:

func colorOverlay(color: UIColor) -> Filter {
    return { image in
        let overlay = colorGenerator(color: color)(image)
        return compositeSourceOver(overlay: overlay)(image)
    }
}复制代码

咱们来用一下试试:

let image = UIImage(named: "graynoel")
let originImage = CIImage(image: image!)!

// 高斯模糊
let radius = 5.0
let blurredImage = blur(radius: radius)(originImage)

// 对 blurredImage 再叠加颜色滤镜、合成滤镜
let overlayColor = UIColor.red.withAlphaComponent(0.2)
let outputImage = colorOverlay(color: overlayColor)(blurredImage)复制代码

效果以下:

效果已经达成!为了方便,对于上面这种将两种滤镜组合应用的状况,能够直接嵌套的来写:

let outputImage = colorOverlay(color: overlayColor)(blur(radius: radius)(originImage))复制代码

复杂的括号嵌套已经让代码失去了可读性,如何优化呢?先来了解一个概念:柯里化(Currying),摘录一段维基中的解释:

In functional programming languages, and many others, it provides a way of automatically managing how arguments are passed to functions and exceptions.

简单来讲,就是把接受多个参数的方法进行一些变形,使其更加灵活的方法。咱们借用王巍(@onevcat)在其《100个Swift开发必备Tip》一书中柯里化 (CURRYING) 章节的例子进行介绍:

// 数字 +1 的函数
func addOne(num: Int) -> Int {
    return num + 1
}复制代码

若是咱们还须要 +二、+三、…… 的函数,这样的写法就没法扩展了,所以能够借助柯里化,用以下方式进行定义:

func addNum(adder: Int) -> (Int) -> Int {
    return { num in
        return num + adder
    }
}复制代码

能够看到,函数返回值变成了 (Int) -> Int,这不正是一等函数的应用吗!咱们再使用时,就能够采用以下方法:

// 方式 1
let addTwo = addNum(adder: 2)
addTwo(1) // 3

// 方式 2
addNum(adder: 2)(3) // 5复制代码

方式 1 中的 addTwo 就是咱们基于 addNum 函数“装配”出的高阶函数,而方式 2 的写法则更简洁。

让咱们再回到滤镜组合应用的问题,借助柯里化,咱们定义如下函数:

func composeFilters(filter1: @escaping Filter, _ filter2: @escaping Filter) -> Filter {
    return { image in
        filter2(filter1(image))
    }
}

// 调用
let composeTwoFilters = composeFilters(filter1: blur(radius: radius), colorOverlay(color: overlayColor))
let outputImage = composeTwoFilters(originImage)复制代码

composeFilters 函数能够将两个 Filter 进行叠加,生成一个新的 Filter,而后传入 originImage 便可获得最终结果。

原书章节还讲解了引入运算符的方式进行代码改写,思路相似,本文再也不展开。


思考

高阶函数

经过两章的学习,咱们对高阶函数(Higher-order function)已经有了一些了解,下面简单总结一下。首先,仍是先看看维基对高阶函数的定义:

In mathematics and computer science, a higher-order function (also functional, functional form or functor) is a function that does at least one of the following:

  • takes one or more functions as arguments (i.e., procedural parameters),
  • returns a function as its result.

高阶函数知足两个条件:

  1. 以一个或多个函数做为参数;
  2. 将一个函数做为返回值。

对比前文的代码,composeFilters 函数就是一个高阶函数,它以两个 Filter 函数做为参数,组合后的滤镜函数做为返回值。

高阶函数应该与函数式思想相结合,函数式思想帮助咱们获得一系列“小型”函数,而后“装配”成复杂的高阶函数,对功能进行扩展。一样的,高阶函数仍然能够做为“小型”函数,继续“装配”更高阶的函数,这种“装配”的便捷性和扩展性就得益于函数式编程方法。

柯里化

柯里化是本章新引入的概念,经过前文代码示例能够看出,借助柯里化,咱们能够开发出用于“量产函数”的函数,就如同开发了一个函数的模板,能够生成各类相似功能的函数,而且避免写出不少重复代码,也方便了后续维护。

可是,对于柯里化的应用是须要进行设计的,反复嵌套或连接过多括号会严重影响代码的可读性,使用时,要根据具体问题适度柯里化,提升开发效率和代码扩展性的同时,避免滥用致使的可读性降低。


参考资料

  1. Github: objcio/functional-swift
  2. Currying
  3. 柯里化 (CURRYING)
  4. Higher-order function

本文属于《函数式 Swift》读书笔记系列,同步更新于 huizhao.win,欢迎关注!