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 都是是安全、孤立且可以重用的。

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

推荐阅读更多精彩内容

  • Core Image是一个强大的框架,它能够让你轻松地对图像进行过滤。你能够通过修改图像的饱和度、色调或曝光率来获...
    木易林1阅读 1,071评论 0 1
  • 许多UIView的子类,如一个UIButton或一个UILabel,它们知道怎么绘制自己。迟早,你也将想要做一些自...
    shenzhenboy阅读 1,563评论 2 8
  • 前言 最近在研究 Core Image 自定义 Filter 相关内容,重新学习了 Core Image,对 Co...
    泥孩儿0107阅读 697评论 0 4
  • 以下是本人通过阅读Grizzly版OpenStack源码,整理的简要的Nova模块源码结构,希望和大家相互交流。 ...
    Chenzongshu阅读 2,592评论 0 50
  • 很多毕业刚出来的小姑娘们,都不太懂化妆,刚出社会,想要漂亮但也没多少经济基础。先是化眼睛还是眉毛,用什么产品好,什...
    我独一你无二阅读 844评论 0 5