Swift 高阶函数
map、flatMap、filter和reduce,几乎实现lambda表达式的语言里都会在集合里增加这些方法,
见swift 学习(一)基础知识 (基本数据类型,操作符,流控制,集合)中的集合
map
通过map
实现元素的映射,好处是我们可以非常清楚的表示两个元素列表作了何种转换,实现起来更简单,却有更大的信噪比。减轻我们理解代码的难度。
使用map操作Container
func map<U>(transform: (T) -> U) -> U[]
它接受一个函数叫做 transform
,然后依次对原数组里的每一个元素调用该函数,函数返回值组成另一个数组:
[ x1, x2, ... , xn].map(f) -> [f(x1), f(x2), ... , f(xn)]
// foreach 表示 var newArray : Array<T> = [] for item in oldArray { newArray += f(item) }
示例
var oldArray = [10,20,45,32] var newArray = oldArray.map({"\($0)€"}) println(newArray) // [¥10, ¥20, ¥45, ¥32]
func hello(someName: String?) -> String { return someName.map { name in "Hello, \(name)" } ?? "Hello world!" }
map是一个高级函数,并不仅仅对数组有意义。它可以在任何类型和方法中实现,包括一种或多种映射方式,一个或多个映射关系。
使用map操作Optional
let number = Optional(815) let transformedNumber = number.map { $0 * 2 }.map { $0 % 2 == 0 } // transformedNumber: Optional.Some(true)
在Optional中使用map函数的好处是,它将为我们自动处理空值,如果我们试图在一个nil值处进行操作,可以先使用optional.map申请转换,如果原来为空的话,最终也将为空,这就可以避免使用if let嵌套打开Optional。
let nilNumber: Int? = .None let transformedNilNumber = nilNumber.map { $0 * 2 }.map { $0 % 2 == 0 } // transformedNilNumber: None
map函数在针对不同的类型时可以有不同的行为,这主要取决于该类型的语义。
用自己写的类型实现map函数
class Box< T> { let unbox: T init(_ value: T) { self.unbox = value } } enum Result< T> { case Value(Box< T>) case Error(NSError) }
该Box类用来绕过当前Swift版本的一处限制(unimplemented IR generation feature non-fixed multi-payload enum layout)。
这是一种在一些语言中被称为Either的实现模式,只有在这种情况下,我们必须使用一个NSError类来代替它,因为我们需要用它来报告我们的操作结果。
从概念上来讲,Result与Optional是非常相似的:可以适用于任意类型的值,无论它是否有意义,然而在这种情况下,Result可以告诉我们无意义的值是什么,且为什么存在。
看下面的例子,读取一个文件的内容,作为Result对象的返回结果:
func dataWithContentsOfFile(file: String, encoding: NSStringEncoding) -> Result { var error: NSError? if let data = NSData(contentsOfFile: file, options: .allZeros, error: &error) { return .Value(Box(data)) } else { return .Error(error!) } }
这个函数将返回一个NSData对象,或一个NSError告知文件无法读取。
如果在以前,我们可能为了读出这些值,需要做一些转换。并且需要检测每一步转换的值是否正确,这可能会导致我们需要使用一些繁琐的if let 或switch嵌套来检测。在这种情况下,我们只需要提供转换方法,如果不这么做,我们也可以传递相同的error。
假设我们要读取一个字符串的内容,我们会得到一个NSData,然后我们需要转化成一个字符串,之后我们将它变成大写:
NSData -> String -> String
let data: Result< NSData> = dataWithContentsOfFile(path, NSUTF8StringEncoding) let uppercaseContents: Result = data.map { NSString(data: $0, encoding: NSUTF8StringEncoding)! }.map { $0.uppercaseString }
这类似于上面使用map函数处理数组的例子,我们只需要描述清楚想要完成的目标即可。
相比之下,下面这份代码是不使用map函数:
let data: Result< NSData> = dataWithContentsOfFile(path, NSUTF8StringEncoding) var stringContents: String? switch data { case let .Value(value): stringContents = NSString(data: value.unbox, encoding: NSUTF8StringEncoding) case let .Error(error): break } let uppercaseContents: String? = stringContents?.uppercaseString
Result.map函数:
extension Result { func map< U>(f: T -> U) -> Result< U> { switch self { case let .Value(value): return Result< U>.Value(Box(f(value.unbox))) case let .Error(error): return Result< U>.Error(error) } } }
flatMap
比map更上一层楼
var oldArray = [10,20,45,32] var newArray = oldArray.flatMap{ ["¥\($0)","$\($0 )"] } println(newArray) // [¥10, $10,¥20, $20, ¥45, $45, ¥32, $32]
可传入n个处理方法,处理后得到n组数据,并组合到同一个数组中
相对的,java8中flatMap 的实现方式有很大不同,java8中,被处理数据是n组元素,处理方法只有一个闭包,同样的结果是组合为一个集合,但组合的次序不一样,是依次处理每一组元素的个体,并归入同一个组。而swift是对一个数组的每个元素进行多种处理,并归入同一个组。
实际上就是对map的扩展,要求在实现元素的映射时,映射的结果同样是一个可以继续映射的类型。
extension Result { static func flatten< T>(result: Result< Result< T>>) -> Result< T> { switch result { case let .Value(innerResult): return innerResult.unbox case let .Error(error): return Result< T>.Error(error) } } }
extension Result { func flatMap< U>(f: T -> Result< U>) -> Result< U> { return Result.flatten(map(f)) } }
filter
filter
就是筛选的功能,参数是一个用来判断是否筛除的筛选闭包
func filter(includeElement: (T) -> Bool) -> [T]
// 传统的 foreach 实现的方法: var oldArray = [10,20,45,32] var filteredArray : Array<Int> = [] for money in oldArray { if (money > 30) { filteredArray.append(money ) } } // 用 filter 可以这样实现: var oldArray = [10,20,45,32] var filteredArray = oldArray.filter({ return $0 > 30 }) println(filteredArray) // [45, 32]
reduce
reduce
函数解决了把数组中的值整合到某个独立对象的问题
func reduce<U>(initial: U, combine: (U, T) -> U) -> U
比如我们要把数组中的值都加起来放到 sum
里
// foreach 实现 var oldArray = [10,20,45,32] var sum = 10 for money in oldArray { sum = sum + money } println(sum) // 117 // reduce var oldArray = [10,20,45,32] var sum = oldArray.reduce(10,{$0 + $1})
习题
Write a function applyTwice(f:(Float -> Float),x:Float) -> Float that takes a function f and a float x and aplies f to x twice i.e. f(f(x))
func applyTwice(f:(Float -> Float),x:Float) -> Float { return f(f(x)) }
Write a function applyKTimes(f:(Float -> Float),x:Float,k:Int) -> Float that takes a function f and a float x and aplies f to x k times
// recursive version func applyKTimes(f:(Float -> Float), x:Float, k:Int) -> Float { return k > 0 ? applyKTimes(f, f(x), k - 1) : x } // unrolled by hand func applyKTimes(f:(Float -> Float),x:Float,k:Int) -> Float { var y : Float = x for _ in 0..<k { y = f(y) } return y }
Using applyKTimes write a function that raises x to the kth power
func getKthPower(x:Float, k:Int) -> Float{ return applyKTimes( {x * $0}, 1, k) } getKthPower(2.0, 3) // 8.0
Given an array of Users which have properties name:String and age:Int write a map function that returns an array of strings consisting of the user’s names
class User { var name: String? var age : Int? init (name: String, age:Int) { self.name = name self.age = age } } var user1 = User(name: "WHY1", age: 22) var user2 = User(name: "WHY2", age: 23) var user3 = User(name: "WHY3", age: 24) var user4 = User(name: "WHY4", age: 25) var users = [user1,user2,user3,user4] var names: [String] = [] users.map({ (user:User) in names.append(user.name!) }) println(names) // [WHY1, WHY2, WHY3, WHY4]
Given an array of of dictionaries containing keys for “name” and “age” write a map function that returns an array of users created from it
var users = [ ["name":"WHY1","age":"22"], ["name":"WHY2","age":"23"], ["name":"WHY3","age":"24"], ["name":"WHY4","age":"25"] ] var result = users.map({ (userDic:[String:String]) -> User in return User(name: userDic["name"]!, age:userDic["age"]!.toInt()!) })
Given an array of numbers write a filter method that only selects odd integers
var nums = [1,2,4,8,23,45,89,127] var odds = nums.filter({ $0 % 2 == 0 }) // 2 4 8
Given an array of strings write a filter function that selects only strings that can be converted to Ints
var strs = ["2333","1223","callmewhy","callherhh"] var intables = strs.filter({ $0.toInt() != nil }) // ["2333", "1223"]
Given an array of UIViews write a filter function that selects only those views that are a subclass of UILabel
import UIKit var view1 = UIView() var view2 = UIView() var view3 = UILabel() var view4 = UIView() var views = [view1,view2,view3,view4] var labels = views.filter({ $0.isKindOfClass(UILabel) }) // view3
Write a reduce function that takes an array of strings and returns a single string consisting of the given strings separated by newlines
var strs = ["str1","str2","str3","str4"] var str = strs.reduce("", combine: { "\($0)\n\($1)" }) println(str)
Write a reduce function that finds the largest element in an array of Ints
var ints = [1,2,3,4,5,6] var maxValue = ints.reduce(Int.min, { max($0, $1) }) // 6
You could implement a mean function using the reduce operation {$0 + $1 / Float(array.count)}. Why is this a bad idea?
var array = [1,2,3,4,6] var mean = array.reduce( 0, combine: {$0 + Float($1) / Float(array.count)} ) // make division 5 times
There’s a problem you encounter when trying to implement a parallel version of reduce. What property should the operation have to make this easier ?
// TODO
Implement Church Numerals in Swift (This is a difficult and open ended exercise)
// TODO
Swift 的函数式 API(转)
在过去的时间里,人们对于设计 API 总结了很多通用的模式和最佳实践方案。一般情况下,我们总是可以从苹果的 Foundation、Cocoa、Cocoa Touch 和很多其他框架中总结出一些开发中的范例。毫无疑问,对于“特定情境下的 API 应该如何设计”这个问题,不同的人总是有着不同的意见,对于这个问题有很大的讨论空间。不过对于很多 Objective-C 的开发者来说,对于那些常用的模式早已习以为常。
随着 Swift 的出现,设计 API 引起了更多的问题。绝大多数情况下,我们只能继续做着手头的工作,然后把现有的方法翻译成 Swift 版本。不过,这对于 Swift 来说并不公平,因为和 Objective-C 相比,Swift 添加了很多新的特性。引用 Swift 创始人 Chris Lattner的一段话:
Swift 引入了泛型和函数式编程的思想,极大地扩展了设计的空间。
在这篇文章里,我们将会围绕 Core Image
进行 API 封装,以此为例,探索如何在 API 设计中使用这些新的工具。 Core Image
是一个功能强大的图像处理框架,但是它的 API 有时有点笨重。 Core Image
的 API 是弱类型的 - 它通过键值对 (key-value) 设置图像滤镜。这样在设置参数的类型和名字时很容易失误,会导致运行时错误。新的 API 将会十分的安全和模块化,通过使用类型而不是键值对来规避这样的运行时错误。
目标
我们的目标是构建一个 API ,让我们可以简单安全的组装自定义滤镜。举个例子,在文章的结尾,我们可以这样写:
let myFilter = blur(blurRadius) >|> colorOverlay(overlayColor)
let result = myFilter(image)
上面构建了一个自定义的滤镜,先模糊图像,然后再添加一个颜色蒙版。为了达到这个目标,我们将充分利用 Swift 函数是一等公民这一特性。项目源码可以在 Github 上的这个示例项目中下载。
Filter 类型
CIFilter
是 Core Image
中的一个核心类,用来创建图像滤镜。当实例化一个 CIFilter
对象之后,你 (几乎) 总是通过kCIInputImageKey
来输入图像,然后通过 kCIOutputImageKey
获取返回的图像,返回的结果可以作为下一个滤镜的参数输入。
在我们即将开发的 API 里,我们会把这些键值对 (key-value) 对应的真实内容抽离出来,为用户提供一个安全的强类型 API。我们定义了自己的滤镜类型 Filter
,它是一个可以传入图片作为参数的函数,并且返回一个新的图片。
typealias Filter = CIImage -> CIImage
这里我们用 typealias
关键字,为 CIImage -> CIImage
类型定义了我们自己的名字,这个类型是一个函数,它的参数是一个CIImage
,返回值也是 CIImage
。这是我们后面开发需要的基础类型。
如果你不太熟悉函数式编程,你可能对于把一个函数类型命名为 Filter
感觉有点奇怪,通常来说,我们会用这样的命名来定义一个类。如果我们很想以某种方式来表现这个类型的函数式的特性,我们可以把它命名成 FilterFunction
或者一些其他的类似的名字。但是,我们有意识的选择了 Filter
这个名字,因为在函数式编程的核心哲学里,函数就是值,函数和结构体、整数、多元组、或者类,并没有任何区别。一开始我也不是很适应,不过一段时间之后发现,这样做确实很有意义。
构建滤镜
现在我们已经定义了 Filter
类型,接下来可以定义函数来构建特定的滤镜了。这些函数需要参数来设置特定的滤镜,并且返回一个类型为 Filter
的值。这些函数大概是这个样子:
func myFilter(/* parameters */) -> Filter
注意返回的值 Filter
本身就是一个函数,在后面有利于我们将多个滤镜组合起来,以达到理想的处理效果。
为了让后面的开发更轻松一点,我们扩展了 CIFilter
类,添加了一个 convenience 的初始化方法,以及一个用来获取输出图像的计算属性:
typealias Parameters = Dictionary<String, AnyObject>
extension CIFilter {
convenience init(name: String, parameters: Parameters) {
self.init(name: name)
setDefaults()
for (key, value : AnyObject) in parameters {
setValue(value, forKey: key)
}
}
var outputImage: CIImage { return self.valueForKey(kCIOutputImageKey) as CIImage }
}
这个 convenience 初始化方法有两个参数,第一个参数是滤镜的名字,第二个参数是一个字典。字典中的键值对将会被设置成新滤镜的参数。我们 convenience 初始化方法先调用了指定的初始化方法,这符合 Swift 的开发规范。
计算属性 outputImage
可以方便地从滤镜对象中获取到输出的图像。它查找 kCIOutputImageKey
对应的值并且将其转换成一个CIImage
对象。通过提供这个属性, API 的用户不再需要对返回的结果手动进行类型转换了。
模糊
有了这些东西,现在我们就可以定义属于自己的简单滤镜了。高斯模糊滤镜只需要一个模糊半径作为参数,我们可以非常容易的完成一个模糊滤镜:
func blur(radius: Double) -> Filter {
return { image in
let parameters : Parameters = [kCIInputRadiusKey: radius, kCIInputImageKey: image]
let filter = CIFilter(name:"CIGaussianBlur", parameters:parameters)
return filter.outputImage
}
}
就是这么简单,这个模糊函数返回了一个函数,新的函数的参数是一个类型为 CIImage
的图片,返回值 (filter.outputImage
) 是一个新的图片 。这个模糊函数的格式是 CIImage -> CIImage
,满足我们前面定义的 Filter
类型的格式。
这个例子只是对 Core Image
中已有滤镜的一个简单的封装,我们可以多次重复同样的模式,创建属于我们自己的滤镜函数。
颜色蒙版
现在让我们定义一个颜色滤镜,可以在现有的图片上面加上一层颜色蒙版。 Core Image
默认没有提供这个滤镜,不过我们可以通过已有的滤镜组装一个。
我们使用两个模块来完成这个工作,一个是颜色生成滤镜 (CIConstantColorGenerator
),另一个是资源合成滤镜 (CISourceOverCompositing
)。让我们先定义一个生成一个常量颜色面板的滤镜:
func colorGenerator(color: UIColor) -> Filter {
return { _ in
let filter = CIFilter(name:"CIConstantColorGenerator", parameters: [kCIInputColorKey: color])
return filter.outputImage
}
}
这段代码看起来和前面的模糊滤镜差不多,不过有一个较为明显的差异:颜色生成滤镜不会检测输入的图片。所以在函数里我们不需要给传入的图片参数命名,我们使用了一个匿名参数 _
来强调这个 filter 的图片参数是被忽略的。
接下来,我们来定义合成滤镜:
func compositeSourceOver(overlay: CIImage) -> Filter {
return { image in
let parameters : Parameters = [
kCIInputBackgroundImageKey: image,
kCIInputImageKey: overlay
]
let filter = CIFilter(name:"CISourceOverCompositing", parameters: parameters)
return filter.outputImage.imageByCroppingToRect(image.extent())
}
}
在这里我们将输出图像裁剪到和输入大小一样。这并不是严格需要的,要取决于我们想让滤镜如何工作。不过,在后面我们的例子中我们可以看出来这是一个明智之举。
func colorOverlay(color: UIColor) -> Filter {
return { image in
let overlay = colorGenerator(color)(image)
return compositeSourceOver(overlay)(image)
}
}
我们再一次返回了一个参数为图片的函数,colorOverlay
在一开始先调用了 colorGenerator
滤镜。colorGenerator
滤镜需要一个颜色作为参数,并且返回一个滤镜。因此 colorGenerator(color)
是 Filter
类型的。但是 Filter
类型本身是一个CIImage
向 CIImage
转换的函数,我们可以在 colorGenerator(color)
后面加上一个类型为 CIImage
的参数,这样可以得到一个类型为 CIImage
的蒙版图片。这就是在定义 overlay
的时候发生的事情:我们用 colorGenerator
函数创建了一个滤镜,然后把图片作为一个参数传给了这个滤镜,从而得到了一张新的图片。返回值 compositeSourceOver(overlay)(image)
和这个基本相似,它由一个滤镜 compositeSourceOver(overlay)
和一个图片参数 image
组成。
组合滤镜
现在我们已经定义了一个模糊滤镜和一个颜色滤镜,我们在使用的时候可以把它们组合在一起:我们先将图片做模糊处理,然后再在上面放一个红色的蒙层。让我们先加载一张图片:
let url = NSURL(string: "http://tinyurl.com/m74sldb");
let image = CIImage(contentsOfURL: url)
现在我们可以把滤镜组合起来,同时应用到一张图片上:
let blurRadius = 5.0
let overlayColor = UIColor.redColor().colorWithAlphaComponent(0.2)
let blurredImage = blur(blurRadius)(image)
let overlaidImage = colorOverlay(overlayColor)(blurredImage)
我们又一次的通过滤镜组装了图片。比如在倒数第二行,我们先得到了模糊滤镜 blur(blurRadius)
,然后再把这个滤镜应用到图片上。
函数组装
不过,我们可以做的比上面的更好。我们可以简单的把两行滤镜的调用组合在一起变成一行,这是我脑海中想到的第一个能改进的地方:
let result = colorOverlay(overlayColor)(blur(blurRadius)(image))
不过,这些圆括号让这行代码完全不具有可读性,更好的方式是定义一个函数来完成这项任务:
func composeFilters(filter1: Filter, filter2: Filter) -> Filter {
return { img in filter2(filter1(img)) }
}
composeFilters
函数的两个参数都是 Filter ,并且返回了一个新的 Filter 滤镜。组装后的滤镜需要一个 CIImage
类型的参数,并且会把这个参数分别传给 filter1
和 filter2
。现在我们可以用 composeFilters
来定义我们自己的组合滤镜:
let myFilter = composeFilters(blur(blurRadius), colorOverlay(overlayColor))
let result = myFilter(image)
我们还可以更进一步的定义一个滤镜运算符,让代码更具有可读性,
infix operator >|> { associativity left }
func >|> (filter1: Filter, filter2: Filter) -> Filter {
return { img in filter2(filter1(img)) }
}
运算符通过 infix
关键字定义,表明运算符具有 左
和 右
两个参数。associativity left
表明这个运算满足左结合律,即:f1 >|> f2 >|> f3 等价于 (f1 >|> f2) >|> f3。通过使这个运算满足左结合律,再加上运算内先应用了左侧的滤镜,所以在使用的时候滤镜顺序是从左往右的,就像 Unix 管道一样。
剩余的部分是一个函数,内容和 composeFilters
基本相同,只不过函数名变成了 >|>
。
接下来我们把这个组合滤镜运算器应用到前面的例子中:
let myFilter = blur(blurRadius) >|> colorOverlay(overlayColor)
let result = myFilter(image)
运算符让代码变得更易于阅读和理解滤镜使用的顺序,调用滤镜的时候也更加的方便。就好比是 1 + 2 + 3 + 4
要比add(add(add(1, 2), 3), 4)
更加清晰,更加容易理解。
自定义运算符
很多 Objective-C 的开发者对于自定义运算符持有怀疑态度。在 Swift 刚发布的时候,这是一个并没有很受欢迎的特性。很多人在 C++ 中遭遇过自定义运算符过度使用 (甚至滥用) 的情况,有些是个人经历过的,有些是听到别人谈起的。
你可能对于前面定义的运算符 >|>
持有同样的怀疑态度,毕竟如果每个人都定义自己的运算符,那代码岂不是很难理解了?值得庆幸的是在函数式编程里有很多的操作,为这些操作定义一个运算符并不是一件很罕见的事情。
我们定义的滤镜组合运算符是一个函数组合的例子,这是一个在函数式编程中广泛使用的概念。在数学里,两个函数 f
和 g
的组合有时候写做 f ∘ g
,这样定义了一种全新的函数,将输入的 x
映射到 f(g(x))
上。这恰好就是我们的 >|>
所做的工作 (除了函数的逆向调用)。
泛型
仔细想想,其实我们并没有必要去定义一个用来专门组装滤镜的运算符,我们可以用一个泛型的运算符来组装函数。目前我们的 >|>
是这样的:
func >|> (filter1: Filter, filter2: Filter) -> Filter
这样定义之后,我们传入的参数只能是 Filter
类型的滤镜。
但是,我们可以利用 Swift 的通用特性来定义一个泛型的函数组合运算符:
func >|> <A, B, C>(lhs: A -> B, rhs: B -> C) -> A -> C {
return { x in rhs(lhs(x)) }
}
这个一开始可能很难理解 -- 至少对我来说是这样。但是分开的看了各个部分之后,一切都变得清晰起来。
首先,我们来看一下函数名后面的尖括号。尖括号定义了这个函数适用的泛型类型。在这个例子里我们定义了三个类型:A、B 和 C。因为我们并没有指定这些类型,所以它们可以代表任何东西。
接下来让我们来看看函数的参数:第一个参数:lhs (left-hand side 的缩写),是一个类型为 A -> B 的函数。这代表一个函数的参数为 A,返回值的类型为 B。第二个参数:rhs (right-hand side 的缩写),是一个类型为 B -> C 的函数。参数命名为 lhs 和 rhs,因为它们分别对应操作符左边和右边的值。
重写了没有 Filter
的滤镜组合运算符之后,我们很快就发现其实前面实现的组合运算符只是泛型函数中的一个特殊情况:
func >|> (filter1: CIImage -> CIImage, filter2: CIImage -> CIImage) -> CIImage -> CIImage
把我们脑海中的泛型类型 A、B、C 都换成 CIImage
,这样可以清晰的理解用通用运算符的来替换滤镜组合运算符是多么的有用。
结论
至此,我们成功的用函数式 API 封装了 Core Image
。希望这个例子能够很好的说明,对于 Objective-C 的开发者来说,在我们所熟知的 API 的设计模式之外有一片完全不同的世界。有了 Swift,我们现在可以动手探索那些全新的领域,并且将它们充分地利用起来。