【函数式 Swift】封装 Core Image

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

本章关键词

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

  • 高阶函数
  • 柯里化

案例:封装 Core Image

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

如果你不了解 CIFilter 的用途,可以先来看看下面这段代码和效果:

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. 防御型代码很多:注释 1、3、6 处;
  2. 使用字符串配置滤镜类型:注释 2 处;
  3. 使用键值方式配置滤镜参数:注释 4、5 处。

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

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

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

这样做可以达到效果,可是并没有使用函数式思想,那函数式思想指导下的解决方案是什么样呢?

借助前一章《函数式思想》的结论,首先应该选择一个合适的类型,而合适的类型直接取决于所要实现功能的输入和输出,很明显,输入、输出都是 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
}复制代码

如果我们还需要 +2、+3、…… 的函数,这样的写法就无法扩展了,因此可以借助柯里化,用如下方式进行定义:

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,欢迎关注!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值