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,我們現在可以動手探索那些全新的領域,並且將它們充分地利用起來。