map、flatMap、filter和reduce,幾乎實現lambda表達式的語言裏都會在集合裏增長這些方法,javascript
見swift 學習(一)基礎知識 (基本數據類型,操做符,流控制,集合)中的集合html
經過map
實現元素的映射,好處是咱們能夠很是清楚的表示兩個元素列表做了何種轉換,實現起來更簡單,卻有更大的信噪比。減輕咱們理解代碼的難度。github
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) }
示例swift
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是一個高級函數,並不只僅對數組有意義。它能夠在任何類型和方法中實現,包括一種或多種映射方式,一個或多個映射關係。設計模式
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。 api
let nilNumber: Int? = .None let transformedNilNumber = nilNumber.map { $0 * 2 }.map { $0 % 2 == 0 } // transformedNilNumber: None
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) } } }
比map更上一層樓
var oldArray = [10,20,45,32] var newArray = oldArray.flatMap{ ["¥\($0)","$\($0 )"] } println(newArray) // [¥10, $10,¥20, $20, ¥45, $45, ¥32, $32]
相對的,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
就是篩選的功能,參數是一個用來判斷是否篩除的篩選閉包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
函數解決了把數組中的值整合到某個獨立對象的問題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
在過去的時間裏,人們對於設計 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 上的這個示例項目中下載。
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,咱們如今能夠動手探索那些全新的領域,而且將它們充分地利用起來。