Functional Programming in Swift(
原文首发于我的blog:https://chengwey.com
Chapter 3 Wrapping Core Image
本文是《Functional Programming in Swift》中第三章的笔记,如果你感兴趣,请购买英文原版。
上一节讨论了高阶函数的概念,并展示了函数作为参数传递。本节我们展示如何使用高阶函数来封装一些面向对象的 API 。Core Image 是一个很强大图像处理框架,我们就用他来开刀重写一个 Filter。
<h2 id='TheFilterType'>1. The Filter Type</h2>
我们使用 Core Image中 的 CIFilter 类来创建 image filters。具体过程为:
- 1.初始化一个 CIFilter 对象实例
- 2.提供一个输入 image,使用字典进行封装,key 为 kCIInputImageKey
- 3.获取结果为一个字典,使用 kCIOutputImageKey 得到最终 image
- 4.然后将该 image 继续作为下一个 filter 的输入参数
我们改写的 API 将封装这些 key-value,提供更安全、便利的 API 给用户。我们首先定义自己的 Filter 类型:
typealias Filter = CIImage -> CIImage
<h2 id='BuildingFilters'>2. Building Filters</h2>
我们有了基本的 Filter 类型,就可以开始定义一些特定的 filter 了,我们可以定义一些函数,根据不同的参数,输出不同的 filter。具体函数形式如下:
func myFilter(/* parameters */) -> Filter
Blur
首先来定义高斯模糊滤镜,该滤镜只需要一个模糊半径(blur radius)做参数
func blur(radius: Double) -> Filter {
return { image in
let parameters = [
kCIInputRadiusKey: radius,
kCIInputImageKey: image
]
let filter = CIFilter(name: "CIGaussianBlur",
withInputParameters: parameters)
return filter.outputImage
}
}
这个 blur 函数的返回值是 Filter 类型,该类型正是我们之前定义的,使用一个CIImage 类型的 image 做输入,并返回一个新的 image。这个例子我们简单封装了 Core Image 中的 API,我们可以使用这种方式来创建自己的 filter functions。
Color Overlay
接着我们来实现一个图层颜色,Core Image 没有默认的相关滤镜,但我们可以通过组合现有 filters 的方式来实现。我们具体构建这个 color overlay 需要使用两个现成的 Filter:
- color generator filter (CIConstantColorGenerator)
- source-over compositing filter (CISourceOverCompositing)
func colorGenerator(color: UIColor) -> Filter {
return { _ in
let parameters = [kCIInputColorKey: color]
let filter = CIFilter(name: "CIConstantColorGenerator",
withInputParameters: parameters)
return filter.outputImage
}
}
该函数与之前的高斯模糊函数只有一点不同,color generator filter 不检查input imgage,因此我们使用了一个无名参数"_"来强调 image 参数被无视忽略掉了。
接着我们继续定义 composite filter:
func compositeSourceOver(overlay: CIImage) -> Filter {
return { image in
let parameters = [kCIInputBackgroundImageKey: image,
kCIInputImageKey: overlay]
let filter = CIFilter(name: "CISourceOverCompositing",
withInputParameters: parameters)
let cropRect = image.extent()
return filter.outputImage.imageByCroppingToRect(cropRect)
}
}
这里,我们修剪了输出 image 的尺寸,使其和输入 image 尺寸匹配,这不是必要步骤,但却是一个更好的选项。最后我们合并这两个filter:
func colorOverlay(color: NSColor) -> Filter {
return { image in
let overlay = colorGenerator(color)(image)
return compositeSourceOver(overlay)(image)
}
}
该函数中的 overlay 可以看做是由 UIColor ->(CIImage -> CIImage) 生成的一个image,然后传入一个 CIImage ->(CIImage -> CIImage) 函数得到最终结果。
<h2 id='ComposingFilters'>3. Composing Filters</h2>
现在有了 blur和color overlay filter,我们可以把他们合在一起用:首先 blur 一张图片,然后把红色图层覆盖在图片上:
先载入一张图片
let url = NSURL(string: "http://tinyurl.com/m74sldb");
let image = CIImage(contentsOfURL: url)
我们可以用链式结构将所有的 filter 连起来
let blurRadius = 5.0
let overlayColor = NSColor.redColor().colorWithAlphaComponent(0.2)
let blurredImage = blur(blurRadius)(image)
let overlaidImage = colorOverlay(overlayColor)(blurredImage)
Function Composition
当然,我们也可以用一行代码来调用两个 filter:
let result = colorOverlay(overlayColor)(blur(blurRadius)(image))
不过这样可读性就变得比较差,更好的方式是自定义一个 filter 合成函数:
func composeFilters(filter1: Filter, filter2: Filter) -> Filter {
return { img in filter2(filter1(img)) }
}
有了上面的composeFilters函数,我们就可以任意合成我们想要的 filter 了,比如我们可以合成一个 myFilter1 滤镜:
let myFilter1 = composeFilters(blur(blurRadius),
colorOverlay(overlayColor))
let result1 = myFilter1(image)
让我们来更进一步增加可读性,swift 允许我们自定义操作符,我们来定义一个 “>>>” 运算符:
infix operator >>> { associativity left } //表运算符顺序
func >>> (filter1: Filter, filter2: Filter) -> Filter {
return { img in filter2(filter1(img)) }
}
//使用
let myFilter2 = blur(blurRadius) >>> colorOverlay(overlayColor)
let result2 = myFilter2(image)
<h2 id='TheoreticalBackgroundCurrying'>4. TheoreticalBackground:Currying</h2>
这一章,我们看到了有两种方式来定义一个带两个参数的 function:
//第一种形式为常见的:
func add1(x: Int, y: Int) -> Int {
return x + y
}
//第二种方式:
func add2(x: Int) -> (Int -> Int) {
return { y in return x + y }
}
add2带一个参数 x,返回一个闭包,然后继续带一个参数 y。二者调用方式也不同:
- add1(1, 2)
- add2(1)(2)
swift中的函数箭头 “->” 是右相关的,也就是说类型 “ A -> B -> C ” 可以看做是 “ A -> (B -> C) ”。add1和add2 展示了一个接受多个参数的函数转换成一系列只接受单个参数的函数,这个过程就称为柯理化(add2 是 add1 的柯理化版本)
还有第三种版本 curry functions:
func add3(x: Int)(y: Int) -> Int {
return x + y
}
// 调用
add3(1)(y: 2)
>3
这里需要主要的就是,调用时必须明确提供第二参数的参数名(这里是y)
为什么要使用“柯理化”呢,目前我们都是将函数作为参数传给另一个函数。如果我们使用“非柯理化”的函数,比如 add1,我们就要一次提供所有的参数。而对于“柯理化”的函数,比如 add2,我们可以选择:提供1个或2个参数。我们本章创建的 filter 都是“柯理化”的,他们都有一个附加的 image 做参数。这种方式写出的 filter,我们也很容易使用操作符 ">>>" 来组合他们。当然,我们也可以写出“非柯理化”的filter,但最后产生的code将变得笨重。
<h2 id='Discussion'>5. Discussion</h2>
本章再一次展示了如何将复杂的code简化成许 code snippets,而这些 code snippets 又能通过 function 很方便地进行重组。还展示了高阶函数在实际案例中的应用。
本章通过这种方式设计的 API 有这么几个优势:
- Safety 不会因为未定义的 keys 或类型转换失败而产生 runtime error
- Modularity 很方便地使用操作符 >>> 进行组合,这样做允许你将复杂的 filter 拆分成功能单一、小巧、可重用的子filter。加之,组合后的filters和他的子模块拥有相同的类型,所以你可以交替使用他们。
- Clarity 尽管之前你可能没有使用过 Core Image,但你不出5分钟就能学会我们定义的这套 API:使用 function 来装配这些简单的 filters。为了得到最终结果,你不需要知道各种“Key”( eg: kCIInputImageKey or kCIInputRadiusKey...etc ),你甚至不需要看文档就能学会如何使用这套API。
我们的 API 展示了一连串 functions 可以定义、组合成一个复杂的 filter,而其中每一个 filter 都是是安全、孤立且可以重用的。