由于使用 Cocoa 框架能夠快速地創(chuàng)建一個可用的應用,這讓許多開發(fā)者都喜歡上了 OS X 或 iOS 開發(fā)。如今即使是小團隊也能設計和開發(fā)復雜的應用,這很大程度上要歸功于這些平臺所提供的工具和框架。Swift 的 Playground 不僅繼承了快速開發(fā)的傳統(tǒng),并且有改變我們設計和編寫 OS X 和 iOS 應用方式的潛力。
向那些還不熟悉這個概念的讀者解釋一下,Swift 的 playground 就像是一個可交互的文檔,在其中你可以輸入 Swift 代碼讓它們立即編譯執(zhí)行。操作結果隨著執(zhí)行的時間線一步步被展示,開發(fā)者能在任何時候輸出和監(jiān)視變量。Playground 既可以在現有的 Xcode 工程中進行創(chuàng)建,也能作為單獨的包存在。
Swift 的 playground 主要還是作為學習這門語言的工具而被重視,然而我們只要關注一下類似項目,如 IPython notebooks,就能看到交互編程環(huán)境在更廣闊的范圍內的潛在應用。從科學研究到機器視覺實驗,這些任務現在都使用了 IPython notebooks。這種方式也被用來探索其他語言的范例,如 Haskell 的函數式編程。
接下來我們將探索 Swift 的 playground 在文檔、測試和快速原型方面的用途。本文使用的所有 Swift playground 源碼可以在這里下載。
Swift 是一個全新的語言,許多人都使用 playground 來了解其語法和約定。不光是語言,Swift 還提供了一個新的標準庫。目前這個標準庫的文檔中對于方法的說明不太詳細,所以雨后春筍般的涌現了許多像 practicalswift.org 標準庫方法列表這樣的資源。
編者注 在這里有一份自動生成和整理的 Swift 標準庫文檔,可以作為參考。
不過通過文檔知道方法的作用是一回事,在代碼中實際調用又是另一回事。特別是許多方法在新語言 Swift 的 collection class 中能表現出有趣的特性,因此如果能在 collections 里實際檢驗它們的作用將非常有幫助。
Playground 展示語法和實時執(zhí)行真實數據的特性,為編寫方法和庫接口提供了很好的機會。為了介紹 Collection 方法的使用,我們創(chuàng)建了一個叫 CollectionOperations.playground 的例子,其中包含了一系列 collection 方法的例子,所有的樣例數據都能實時修改。
例如,我們創(chuàng)建了如下的初始數組:
let testArray = [0, 1, 2, 3, 4]
然后想試試 filter()
方法:
let odds = testArray.filter{$0 % 2 == 1}
最后一行顯示這個操作所得到的結果的數組為: [1, 3]
。通過實時編譯我們能了解語法、寫出例子以及獲得方法如何使用的說明,所有這些就如一個活的文檔展示在眼前。
這對于其他的蘋果框架和第三方庫都奏效。 例如,你可能想給其他人展示如何使用 Scene Kit,這是蘋果提供的一個非常棒的框架,它能在 Mac 和 iOS 上快速構建3D場景?;蛟S你會寫一個示例應用,不過這樣展示的時候就要構建和編譯。
在例子 SceneKitMac.playground 中,我們已經建立了一個功能完備帶動畫的 3D 場景。你需要打開 Assistant Editor (在菜單上依次點擊 View | Assistant Editor | Show Assistant Editor),3D 效果和動畫將會被自動渲染。這不需要編譯循環(huán),而且任何的改動,比如改變顏色、幾何形狀、亮度等,都能實時反映出來。使用它能在一個交互例子中很好的記錄和介紹如何使用框架。
除了展示方法和方法的操作,你還會注意到通過檢查輸出的結果,我們可以驗證一個方法的執(zhí)行是否正確,甚至在加載到 playground 的時候就能判斷方法是否被正確解析。不難想象我們也可以在 playground 里添加斷言,以及創(chuàng)建真正的單元測試?;蛘吒M一步,創(chuàng)建出符合條件的測試,從而在你打字時就實現測試驅動開發(fā)。
事實上,在 2014 年 7 月號的 PragPub 雜志中,Ron Jeffries 在他的文章 “從測試驅動開發(fā)角度來看Swift” 中提到過這一觀點:
Playground 很大程度上會對我們如何執(zhí)行測試驅動開發(fā)產生影響。Playground 能夠快速展示我們所能做的東西,因此我們將比之前走得更快。但是同過去的測試驅動開發(fā)框架結合在一起時,能否走的更好?我們是否能提煉出更好的代碼,以滿足更少的缺陷數量和重構?
關于代碼質量的問題還是留給別人回答吧,接下來我們一起來看看 playground 如何加快一個快速原型的開發(fā)。
Accelerate 框架包括了許多功能強大的并行處理大型數據集的方法。這些方法可以利用例如 Intel 芯片中的 SSE 指令集,或者 ARM 芯片中的 NEON 技術等,這樣的現代 CPU 中矢量處理指令的優(yōu)勢。然而,相較于功能的強大,它們的接口似乎有點不透明,其使用的文檔也有點缺乏。這就導致許多開發(fā)者無法使用 Accelerate 這個強大的工具所帶來的優(yōu)勢。
Swift 提供了一個機會,通過方法重載或為 Accelerate 框架進行包裝后,可以讓交互更加容易。這已經在 Chris Liscio 的庫 SMUGMath 的實踐中被證實,這也正是我們接下來將要創(chuàng)建的原型的靈感來源。
假設你有一系列正弦波的數據樣本,然后想通過這些數據來確定這個正弦波的頻率和幅度,你會怎么做呢?一個解決方案是通過傅里葉變換來算出這些值,傅里葉變換能從一個或多個重疊的正弦波提取頻率和幅度信息。Accelerate 框架提供了另一個解決方案,叫做快速傅里葉變換 (FFT),關于這個方案這里有一個 (基于 IPython notebook 的) 很好的解釋。
我們在例子 AccelerateFunctions.playground 中實現了這個原型,你可以對照這個例子來看下面的內容。請確認你已經打開 Assistant Editor (在菜單上依次點擊 View | Assistant Editor | Show Assistant Editor) 以查看每一階段所產生的圖形。
首先我們要產生一些用于實驗的示例波形。使用 Swift 的 map()
方法可以很容易地實現:
let sineArraySize = 64
let frequency1 = 4.0
let phase1 = 0.0
let amplitude1 = 2.0
let sineWave = (0..<sineArraySize).map {
amplitude1 * sin(2.0 * M_PI / Double(sineArraySize) * Double($0) * frequency1 + phase1)
}
為了便于之后使用 FFT,我們的初始數組大小必須是 2 的冪次方。把 sineArraySize
值改為像 32,128 或 256 將改變之后顯示的圖像的密度,但它不會改變計算的基本結果。
要繪制我們的波形,我們將使用新的 XCPlayground 框架 (需要先導入) 和以下輔助函數:
func plotArrayInPlayground<T>(arrayToPlot:Array<T>, title:String) {
for currentValue in arrayToPlot {
XCPCaptureValue(title, currentValue)
}
}
當我們執(zhí)行:
plotArrayInPlayground(sineWave, "Sine wave 1")
我們可以看到如下所示的圖表:
http://wiki.jikexueyuan.com/project/objc/images/16-1.png" alt="" />
這是一個頻率為 4.0、振幅為 2.0、相位為 0 的正弦波。為了變得更有趣一些,我們創(chuàng)建了第二個正弦波,它的頻率為 1.0、振幅為 1.0、相位為 π/2,然后把它疊加到第一個正弦波上:
let frequency2 = 1.0
let phase2 = M_PI / 2.0
let amplitude2 = 1.0
let sineWave2 = (0..<sineArraySize).map {
amplitude2 * sin(2.0 * M_PI / Double(sineArraySize) * Double($0) * frequency2 + phase2)
}
http://wiki.jikexueyuan.com/project/objc/images/16-2.png" alt="" />
現在我們要將兩個波疊加。從這里開始 Accelerate 將幫助我們完成工作。將兩個個獨立地浮點數數組相加非常適進行合并行處理。這里我們要使用到 Accelerate 的 vDSP 庫,它正好有這類功能的方法。為了讓這一切更有趣,我們將重載一個 Swift 操作符用于向量疊加。不巧的是 +
這個操作符已經用于數組連接 (其實挺容易混淆的),而 ++
更適合作為遞增運算符,因此我們將定義 +++
作為相加的運算符。
infix operator +++ {}
func +++ (a: [Double], b: [Double]) -> [Double] {
assert(a.count == b.count, "Expected arrays of the same length, instead got arrays of two different lengths")
var result = [Double](count:a.count, repeatedValue:0.0)
vDSP_vaddD(a, 1, b, 1, &result, 1, UInt(a.count))
return result
}
上文定義了一個操作符,操作符能將兩個 Double
類型的 Swift 數組中的元素依次合并為一個數組。在運算中創(chuàng)建了一個和輸入的數組長度相等的空白數組(假設輸入的兩個數組長度相等)。由于 Swift 的一維數組可以直接映射成 C 語言的數組,因此我們只需要將作為參數的 Doubles
類型數組直接傳遞給 vDSP_vaddD()
方法,并在我們的數組結果前加前綴 &
。
為了驗證上述疊加是否被正確執(zhí)行,我們可以使用 for 循環(huán)以及 Accelerate 方法來繪制合并后的正弦波的結果:
var combinedSineWave = [Double](count:sineArraySize, repeatedValue:0.0)
for currentIndex in 0..<sineArraySize {
combinedSineWave[currentIndex] = sineWave[currentIndex] + sineWave2[currentIndex]
}
let combinedSineWave2 = sineWave +++ sineWave2
plotArrayInPlayground(combinedSineWave, "Combined wave (loop addition)")
plotArrayInPlayground(combinedSineWave2, "Combined wave (Accelerate)")
http://wiki.jikexueyuan.com/project/objc/images/16-3.png" alt="" />
果然,結果是一致的。
在繼續(xù) FFT 本身之前,我們需要另一個向量運算來處理計算的結果。Accelerate 的 FFT 實現中獲取的所有結果都是平方之后的,所以我們需要對它們做平方根操作。我們需要對數組中的所有元素調用類似 sqrt()
方法,這聽上去又是一個使用 Accelerate 的機會。
Accelerate 的 vecLib 庫中有很多等價的數學方法,包括平方根的 vvsqrt()
。這是個使用方法重載的好例子,讓我們來創(chuàng)建一個新版本的 sqrt()
,使其能處理 Double
類型的數組。
func sqrt(x: [Double]) -> [Double] {
var results = [Double](count:x.count, repeatedValue:0.0)
vvsqrt(&results, x, [Int32(x.count)])
return results
}
和我們的疊加運算符一樣,重載的平方函數輸入一個 Double
數組,為輸出創(chuàng)建了一個 Double
類型的數組,并將輸入數組中的所有參數直接傳遞給 Accelerate 中的 vvsqrt()
。通過在 playground 中輸入以下代碼,我們可以驗證剛剛重載的方法。
sqrt(4.0)
sqrt([4.0, 3.0, 16.0])
我們能看到,標準 sqrt()
函數返回2.0,而我們的新建的重載方法返回了 [2.0, 1.73205080756888, 4.0]。這的確是一個非常易用的重載方法,你甚至可以想象照以上方法使用 vecLib 為所有的數學方法寫一個并行的版本 (不過 Mattt Thompson 已經做了這件事)。在一臺 15 寸的 2012 年中的 i7 版本 MacBook Pro 中處理一個有一億個元素的數組,使用基于 Accelerate 的 sqrt()
方法的運行速度比迭代使用普通的一維 sqrt()
快將近一倍。
有了這個以后,我們來實現 FFT。我們并不打算在 FFT 設置的細節(jié)上花費大量時間,以下是我們的 FFT 方法:
let fft_weights: FFTSetupD = vDSP_create_fftsetupD(vDSP_Length(log2(Float(sineArraySize))), FFTRadix(kFFTRadix2))
func fft(var inputArray:[Double]) -> [Double] {
var fftMagnitudes = [Double](count:inputArray.count, repeatedValue:0.0)
var zeroArray = [Double](count:inputArray.count, repeatedValue:0.0)
var splitComplexInput = DSPDoubleSplitComplex(realp: &inputArray, imagp: &zeroArray)
vDSP_fft_zipD(fft_weights, &splitComplexInput, 1, vDSP_Length(log2(CDouble(inputArray.count))), FFTDirection(FFT_FORWARD));
vDSP_zvmagsD(&splitComplexInput, 1, &fftMagnitudes, 1, vDSP_Length(inputArray.count));
let roots = sqrt(fftMagnitudes) // vDSP_zvmagsD returns squares of the FFT magnitudes, so take the root here
var normalizedValues = [Double](count:inputArray.count, repeatedValue:0.0)
vDSP_vsmulD(roots, vDSP_Stride(1), [2.0 / Double(inputArray.count)], &normalizedValues, vDSP_Stride(1), vDSP_Length(inputArray.count))
return normalizedValues
}
第一步,我們設置了計算中需要使用到的 FFT 權重,它和我們要處理的數組大小相關。這些權重將在稍后實際的 FFT 計算中被使用到,它可以通過 vDSP_create_fftsetupD()
計算得到,并且對于給定大小的數組是可以重用的。因為在這里數組的大小是個恒定的常量,因此我們只需要計算一次權重,并將它作為全局變量并在每次 FFT 中重用即可。
在 FFT 方法中,我們初始化了一個用于存放操作結果的數組 fftMagnitudes
,數組的初始元素都為 0,大小為之前正弦波的大小。FFT 運算的輸入參數都是實部加上虛部的復數形式,但我們真正關心的只是它的實數部分,因此我們初始化 splitComplexInput
的時候使用輸入數組作為實數部分,而將零作為虛數部分。然后 vDSP_fft_zipD()
和 vDSP_zvmagsD()
負責執(zhí)行 FFT,并使用 fftMagnitudes
數組來存儲 FFT 從 FFT 中得到的結果的平方數。
在這里,我們使用了之前提到的基于 Accelerate 的 sqrt()
方法來計算平方根,返回實際大小,然后基于輸入數組的大小對值進行歸一化。
對一個單一的正弦波,以上所有操作的的結果如下:
http://wiki.jikexueyuan.com/project/objc/images/16-4.png" alt="" />
疊加的正弦波看起來像這樣:
http://wiki.jikexueyuan.com/project/objc/images/16-5.png" alt="" />
對這些值一個非常簡單的解釋是:這些結果表示了正弦波頻率的集合,從左邊開始,集合中的值表示了在該頻率下檢測到的波的振幅。它們關于中心對稱,因此你可以忽略圖中右半部分的值。
可以觀察到對于頻率為 4.0 振幅為 2.0 的波,在 FFT 中是一個 位于 4 對應于 2.0 的值。同樣對于頻率為 1.0 振幅為 1.0 的波,在 FFT 中是位于 1 對應值為 1.0 的點。盡管疊加后的正弦波得到的 FFT 波形比較復雜,但是依然能夠清晰地區(qū)分合并的兩個波在各自集合內的振幅和頻率,就仿佛它們的 FFT 結果是分別被加入的一樣。
再次強調,這是 FFT 運算的簡化版本,在上文的 FFT 代碼中有簡化操作,但關鍵是在 playground 中通過一步步創(chuàng)建方法,我們能輕松地探索一個復雜的信號處理操作,并且每一步操作的測試都能立即得到圖形反饋。
我們希望這些例子能夠說明 Swift playground 在實踐新類庫和新概念上的作用。
上一個例子中的每一步里,我們都能在執(zhí)行時通過時間線中的圖案來觀察中間數組的狀態(tài)。這對于一個示例程序來說作用非常大,而且也以某種方式為程序提供了界面。所有這些圖像都實時更新,因此你能隨時返回到實現中并修改其中一個波的頻率或振幅,然后看著波形隨著處理步驟變化。這縮短了開發(fā)周期,并且對計算過程的體驗提供了巨大幫助。
這種立即反饋的交互式開發(fā)是為復雜的算法創(chuàng)建原型的很好的案例。在將這樣的復雜算法部署到實際的應用之前,我們有機會在 playground 中對它進行驗證和研究。