更多優(yōu)秀譯文請(qǐng)關(guān)注我們的微信公眾號(hào):learnSwift
原文連接:Efficient JSON in Swift with Functional Concepts and Generics
就在幾個(gè)月前,蘋果推出了一門全新的編程語(yǔ)言,其名為Swift, 這讓我們對(duì)未來(lái) iOS 和 OS X 開(kāi)發(fā)充滿了期待與興奮。人們紛紛開(kāi)始使用 Xcode Beta1 版本來(lái)進(jìn)行 Swift 開(kāi)發(fā),但是很快就發(fā)現(xiàn)解析 JSON 這一常見(jiàn)的操作在 Swift 中并不如在 Objectitve-C 中那樣快捷和方便。Swift 是一門靜態(tài)類型的語(yǔ)言,這意味我們不能簡(jiǎn)單地將對(duì)象賦值給一個(gè)特定類型的變量,并且讓編譯器相信這些對(duì)象就是我們所聲明的那種類型。在 Swift 當(dāng)中,編譯器會(huì)進(jìn)行檢查,以確保我們不會(huì)意外地觸發(fā)運(yùn)行時(shí)錯(cuò)誤。這使得我們可以依賴編譯器來(lái)寫出一些無(wú) bug 的代碼,同時(shí)我們必須做許多額外的工作來(lái)使編譯器不報(bào)錯(cuò)。在這篇文章當(dāng)中,我將使用函數(shù)式思想和泛型來(lái)探討如何編寫易讀高效的 JSON 解析代碼。
請(qǐng)求用戶(User)模型
我們要做的事就是將網(wǎng)絡(luò)請(qǐng)求獲得的數(shù)據(jù)解析成 JSON。之前我們一直使用的是 NSJSONSerialization.JSONObjectWithData(NSData, Int, &NSError)方法,這個(gè)方法返回一個(gè)可選的 JSON 數(shù)據(jù)類型,如果解析過(guò)程出錯(cuò)會(huì)得到 NSError 類型的數(shù)據(jù)。在 Objective-C 當(dāng)中,JSON 的數(shù)據(jù)類型是一個(gè)可以包含任何其它數(shù)據(jù)類型的 NSDictionary類型。 而在 Swift 當(dāng)中, 新的字典類型要求我們必須顯式指定它所包含的數(shù)據(jù)的類型。JSON 數(shù)據(jù)被指定為Dictionary<String, AnyObject>類型。這里使用 AnyObject的原因是 JSON 的值有可能為 String、Double、 Bool、 Array、 Dictionary 或者 null。當(dāng)我們使用 JSON 來(lái)生成模型數(shù)據(jù)時(shí),必須對(duì)每一個(gè)從 JSON 字典中獲取到的值進(jìn)行判斷,以確保這個(gè)值與我們模型中屬性的類型一致。
下面我們來(lái)看一個(gè)用戶(user)的模型:
struct User {
let id: Int
let name: String
let email: String
}
然后,來(lái)看一下對(duì)當(dāng)前用戶的請(qǐng)求和響應(yīng)代碼:
func getUser(request: NSURLRequest, callback: (User) -> ()) {
let task = NSURLSession.sharedSession().dataTaskWithRequest(request)
{ data, urlResponse, error in
var jsonErrorOptional: NSError?
let jsonOptional: AnyObject! =
NSJSONSerialization.JSONObjectWithData(data,
options: NSJSONReadingOptions(0), error: &jsonErrorOptional)
if let json = jsonOptional as? Dictionary<String, AnyObject> {
if let id = json["id"] as AnyObject? as? Int {
// 在 beta5 中,存在一個(gè) bug,所以我們首先要強(qiáng)行轉(zhuǎn)換成 AnyObject?
if let name = json["name"] as AnyObject? as? String {
if let email = json["email"] as AnyObject? as? String {
let user = User(id: id, name: name, email: email)
callback(user)
}
}
}
}
}
task.resume()
}
在一長(zhǎng)串的if-let語(yǔ)句之后,我們終于拿到User對(duì)象??梢韵胂笠幌拢绻粋€(gè)模型的屬性很多,這些代碼會(huì)有多丑。并且,這里我們沒(méi)有進(jìn)行錯(cuò)誤處理,這意味著,只要其中一步出錯(cuò)我們就獲取不到任何數(shù)據(jù)。最后并且最重要的一點(diǎn)是,我們必須對(duì)每個(gè)需要從網(wǎng)絡(luò) API 中獲取的模型寫一遍類似上面這樣的代碼,這將會(huì)導(dǎo)致很多重復(fù)代碼。
在對(duì)代碼進(jìn)行重構(gòu)之前,讓我們先對(duì)JSON的幾種類型定義別名,以使之后的代碼看起來(lái)更簡(jiǎn)潔。
typealias JSON = AnyObject
typealias JSONDictionary = Dictionary<String, JSON>
typealias JSONArray = Array<JSON>
重構(gòu):添加錯(cuò)誤處理
首先,我們將通過(guò)學(xué)習(xí)第一個(gè)函數(shù)式編程的概念,Either<A, B>類型,來(lái)對(duì)代碼進(jìn)行重構(gòu),以使其能進(jìn)行錯(cuò)誤處理。這可以使代碼在正確的情況下返回用戶對(duì)象,而在出錯(cuò)時(shí)返回一個(gè)錯(cuò)誤對(duì)象。在 Swift 當(dāng)中可以使用如下方法來(lái)實(shí)現(xiàn) Either<A, B> :
enum Either<A, B> {
case Left(A)
case Right(B)
}
我們可以使用 Either<NSError, User> 作為傳入回調(diào)的參數(shù),這樣調(diào)用者便可以直接處理解析過(guò)的User對(duì)象或者錯(cuò)誤。
func getUser(request: NSURLRequest, callback:
(Either<NSError, User>) -> ()) {
let task = NSURLSession.sharedSession().dataTaskWithRequest(request)
{ data, urlResponse, error in
// 如果響應(yīng)返回錯(cuò)誤,我們將把錯(cuò)誤發(fā)送給回調(diào)
if let err = error {
callback(.Left(err))
return
}
var jsonErrorOptional: NSError?
let jsonOptional: JSON! =
NSJSONSerialization.JSONObjectWithData(data,
options: NSJSONReadingOptions(0), error: &jsonErrorOptional)
// 如果我們不能解析 JSON,就將發(fā)送回去一個(gè)錯(cuò)誤
if let err = jsonErrorOptional {
callback(.Left(err))
return
}
if let json = jsonOptional as? JSONDictionary {
if let id = json["id"] as AnyObject? as? Int {
if let name = json["name"] as AnyObject? as? String {
if let email = json["email"] as AnyObject? as? String {
let user = User(id: id, name: name, email: email)
callback(.Right(user))
return
}
}
}
}
// 如果我們不能解析所有的屬性,就將發(fā)送回去一個(gè)錯(cuò)誤
callback(.Left(NSError()))
}
task.resume()
}
現(xiàn)在調(diào)用getUser的地方可以直接使用Either,然后對(duì)接收到的用戶對(duì)象進(jìn)行處理,或者直接顯示錯(cuò)誤。
getUser(request) { either in
switch either {
case let .Left(error):
//顯示錯(cuò)誤信息
case let .Right(user):
//對(duì)user進(jìn)行操作
}
}
我們假設(shè)Left一直是NSError,這可以進(jìn)一步簡(jiǎn)化代碼。我們可以使用一個(gè)不同的類型 Result<A> 來(lái)保存我們需要的類型數(shù)據(jù)和錯(cuò)誤信息。它的實(shí)現(xiàn)方式如下:
enum Result<A> {
case Error(NSError)
case Value(A)
}
在當(dāng)前的 Swift 版本(Beta 5)中,上面的 Result類型會(huì)造成編譯錯(cuò)誤(譯者注:事實(shí)上,在 Swift 1.2 中還是有錯(cuò)誤)。 Swift 需要知道存儲(chǔ)在enum當(dāng)中數(shù)據(jù)的確切類型。可以通過(guò)創(chuàng)建一個(gè)靜態(tài)類作為包裝類型來(lái)解決這個(gè)問(wèn)題:
final class Box<A> {
let value: A
init(_ value: A) {
self.value = value
}
}
enum Result<A> {
case Error(NSError)
case Value(Box<A>)
}
將 Either 替換為 Result,代碼將變成這樣:
func getUser(request: NSURLRequest, callback: (Result<User>) -> ()) {
let task = NSURLSession.sharedSession().dataTaskWithRequest(request)
{ data, urlResponse, error in
// 如果響應(yīng)返回錯(cuò)誤,我們將把錯(cuò)誤發(fā)送給回調(diào)
if let err = error {
callback(.Error(err))
return
}
var jsonErrorOptional: NSError?
let jsonOptional: JSON! =
NSJSONSerialization.JSONObjectWithData(data,
options: NSJSONReadingOptions(0), error: &jsonErrorOptional)
// 如果我們不能解析 JSON,就返回一個(gè)錯(cuò)誤
if let err = jsonErrorOptional {
callback(.Error(err))
return
}
if let json = jsonOptional as? JSONDictionary {
if let id = json["id"] as AnyObject? as? Int {
if let name = json["name"] as AnyObject? as? String {
if let email = json["email"] as AnyObject? as? String {
let user = User(id: id, name: name, email: email)
callback(.Value(Box(user)))
return
}
}
}
}
// 如果我們不能解析所有的屬性,就返回一個(gè)錯(cuò)誤
callback(.Error(NSError()))
}
task.resume()
}
getUser(request) { result in
switch result {
case let .Error(error):
// 顯示錯(cuò)誤信息
case let .Value(boxedUser):
let user = boxedUser.value
// 對(duì) user 繼續(xù)操作
}
}
改變不是很大,我們繼續(xù)努力。
重構(gòu): 消除多層嵌套
接下來(lái),我們將為每個(gè)不同的類型創(chuàng)建一個(gè) JSON 解析器來(lái)消滅掉那些丑陋的解析 JSON 的代碼。在這個(gè)對(duì)象中我們只用到了 String, Int 和 Dictionary 三種類型,所以我們需要三個(gè)函數(shù)來(lái)對(duì)這三種類型進(jìn)行解析。
func JSONString(object: JSON?) -> String? {
return object as? String
}
func JSONInt(object: JSON?) -> Int? {
return object as? Int
}
func JSONObject(object: JSON?) -> JSONDictionary? {
return object as? JSONDictionary
}
現(xiàn)在,解析 JSON 的代碼看起來(lái)應(yīng)該是這樣的:
if let json = JSONObject(jsonOptional) {
if let id = JSONInt(json["id"]) {
if let name = JSONString(json["name"]) {
if let email = JSONString(json["email"]) {
let user = User(id: id, name: name, email: email)
}
}
}
}
即使使用了這些函數(shù),還是需要用到一大堆的 if-let 語(yǔ)句。函數(shù)式編程中的 Monads,Applicative Functors,以及 Currying 概念可以幫助我們來(lái)壓縮這段代碼。首先看看與 Swift 中的可選類型十分相似的 Monad。Monad 中有一個(gè)綁定(bind)運(yùn)行符,這個(gè)運(yùn)行符可以給一個(gè)可選類型綁定一個(gè)函數(shù),這個(gè)函數(shù)接受一個(gè)非可選類型參數(shù),并返回一個(gè)可選類型的返回值。如果第一個(gè)可選類型是 .None這個(gè)運(yùn)行符會(huì)返回 .None ,否則它會(huì)對(duì)這個(gè)可選類型進(jìn)行解包,并使用綁定的函數(shù)調(diào)用解包后的數(shù)據(jù)。
infix operator >>> { associativity left precedence 150 }
func >>><A, B>(a: A?, f: A -> B?) -> B? {
if let x = a {
return f(x)
} else {
return .None
}
}
在其它的函數(shù)式語(yǔ)言中,都是使用 >>= 來(lái)作為綁定(bind)運(yùn)算符,但是在 Swift 中這個(gè)運(yùn)算符被用于二進(jìn)制位的移位操作,所以我們使用了 >>> 來(lái)作為替代。在 JSON 代碼中使用這個(gè)操作符可以得到如下代碼:
if let json = jsonOptional >>> JSONObject {
if let id = json["id"] >>> JSONInt {
if let name = json["name"] >>> JSONString {
if let email = json["email"] >>> JSONString {
let user = User(id: id, name: name, email: email)
}
}
}
}
接著就可以去掉解析函數(shù)里的可選參數(shù):
func JSONString(object: JSON) -> String? {
return object as? String
}
func JSONInt(object: JSON) -> Int? {
return object as? Int
}
func JSONObject(object: JSON) -> JSONDictionary? {
return object as? JSONDictionary
}
Functors 有一個(gè)fmap運(yùn)算符,可以在某些上下文中通過(guò)函數(shù)應(yīng)用到解包后的值上面。Applicative Functors 也有apply運(yùn)算符,可以在某些上下文中通過(guò)解包后的函數(shù)應(yīng)用到解包后的值上面。這里的上下文是一個(gè)包含了值的可選值。這就意味著我們可以使用一個(gè)能夠帶有多個(gè)非可選值的函數(shù)來(lái)連接多個(gè)可選值。如果所有的值都存在,.Some會(huì)得到可選值解包的結(jié)果。如果其中任何值是.None,我們將得到.None。可以在 Swift 中像下面這樣定義這些運(yùn)算符:
infix operator <^> { associativity left } // Functor's fmap (usually <$>)
infix operator <*> { associativity left } // Applicative's apply
func <^><A, B>(f: A -> B, a: A?) -> B? {
if let x = a {
return f(x)
} else {
return .None
}
}
func <*><A, B>(f: (A -> B)?, a: A?) -> B? {
if let x = a {
if let fx = f {
return fx(x)
}
}
return .None
}
先別著急使用這些代碼,由于 Swift 不支持自動(dòng)柯里化(auto-currying), 我們需要手動(dòng)柯里化(curry)結(jié)構(gòu)體User中的init方法??吕锘囊馑际钱?dāng)我們給定一個(gè)函數(shù)的參數(shù)比它原來(lái)的參數(shù)更少時(shí),這個(gè)函數(shù)將返回一個(gè)包含剩余參數(shù)的函數(shù)。我們的User模型將看起來(lái)像這樣:
struct User {
let id: Int
let name: String
let email: String
static func create(id: Int)(name: String)(email: String) -> User {
return User(id: id, name: name, email: email)
}
}
把以上代碼合并到一起,我們的 JSON 解析現(xiàn)在看起來(lái)是這樣的:
if let json = jsonOptional >>> JSONObject {
let user = User.create <^>
json["id"] >>> JSONInt <*>
json["name"] >>> JSONString <*>
json["email"] >>> JSONString
}
如果我們解析器的任何部分返回.None,那么user就會(huì)是.None。這看起來(lái)已經(jīng)好多了,但是我們還沒(méi)有優(yōu)化完畢。
到目前為止,我們的getUser函數(shù)看起來(lái)像這樣:
func getUser(request: NSURLRequest, callback: (Result<User>) -> ()) {
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, urlResponse, error in
// 如果響應(yīng)返回錯(cuò)誤,返回錯(cuò)誤
if let err = error {
callback(.Error(err))
return
}
var jsonErrorOptional: NSError?
let jsonOptional: JSON! = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(0), error: &jsonErrorOptional)
// 如果我們不能解析 JSON,返回錯(cuò)誤
if let err = jsonErrorOptional {
callback(.Error(err))
return
}
if let json = jsonOptional >>> JSONObject {
let user = User.create <^>
json["id"] >>> JSONInt <*>
json["name"] >>> JSONString <*>
json["email"] >>> JSONString
if let u = user {
callback(.Value(Box(u)))
return
}
}
// 如果我們不能解析所有的屬性,就返回錯(cuò)誤
callback(.Error(NSError()))
}
task.resume()
}
重構(gòu):通過(guò)綁定消除多個(gè)返回
觀察到在上面的函數(shù)中,我們的調(diào)用了callback函數(shù) 4 次。漏掉任何一次都會(huì)制造 bug。我們可以把這個(gè)函數(shù)分解成 3 個(gè)互不相關(guān)的部分,從而消除潛在的 bug 并重構(gòu)這個(gè)函數(shù)。這三個(gè)部分是:解析響應(yīng),解析數(shù)據(jù)為 JSON 和解析 JSON 為User對(duì)象。這些步驟中的每一步都帶有一個(gè)輸入和返回下一個(gè)步驟的輸入或者錯(cuò)誤。綁定我們的Result類型看起來(lái)是一個(gè)不錯(cuò)的方案。
parseResponse函數(shù)需要Result數(shù)據(jù)和響應(yīng)的狀態(tài)碼。iOS API 只提供了NSURLResponse并保證數(shù)據(jù)獨(dú)立。所以我們創(chuàng)建一個(gè)小結(jié)構(gòu)體來(lái)輔助一下:
struct Response {
let data: NSData
let statusCode: Int = 500
init(data: NSData, urlResponse: NSURLResponse) {
self.data = data
if let httpResponse = urlResponse as? NSHTTPURLResponse {
statusCode = httpResponse.statusCode
}
}
}
現(xiàn)在我們可以把Response結(jié)構(gòu)體傳入parseResponse函數(shù),然后在處理數(shù)據(jù)之前處理錯(cuò)誤。
func parseResponse(response: Response) -> Result<NSData> {
let successRange = 200..<300
if !contains(successRange, response.statusCode) {
return .Error(NSError()) // 自定義你想要的錯(cuò)誤信息
}
return .Value(Box(response.data))
}
下一個(gè)函數(shù)需要我們將一個(gè)可選值轉(zhuǎn)換成Result類型,我們先來(lái)抽象一下。
func resultFromOptional<A>(optional: A?, error: NSError) -> Result<A> {
if let a = optional {
return .Value(Box(a))
} else {
return .Error(error)
}
}
接下來(lái)的函數(shù)需要解析數(shù)據(jù)為 JSON:
func decodeJSON(data: NSData) -> Result<JSON> {
let jsonOptional: JSON! =
NSJSONSerialization.JSONObjectWithData(data,
options: NSJSONReadingOptions(0), error: &jsonErrorOptional)
return resultFromOptional(jsonOptional, NSError())
// 使用默認(rèn)的錯(cuò)誤或者自定義錯(cuò)誤信息
}
然后,我們?cè)?code>User類型中添加 JSON 到User類型的轉(zhuǎn)換:
struct User {
let id: Int
let name: String
let email: String
static func create(id: Int)(name: String)(email: String) -> User {
return User(id: id, name: name, email: email)
}
static func decode(json: JSON) -> Result<User> {
let user = JSONObject(json) >>> { dict in
User.create <^>
dict["id"] >>> JSONInt <*>
dict["name"] >>> JSONString <*>
dict["email"] >>> JSONString
}
return resultFromOptional(user, NSError()) // 自定義錯(cuò)誤消息
}
}
合并代碼之前,需要擴(kuò)展一下綁定, 讓>>>來(lái)配合Result類型:
func >>><A, B>(a: Result<A>, f: A -> Result<B>) -> Result<B> {
switch a {
case let .Value(x): return f(x.value)
case let .Error(error): return .Error(error)
}
}
然后我們添加一個(gè)Result的自定義構(gòu)造器:
enum Result<A> {
case Error(NSError)
case Value(Box<A>)
init(_ error: NSError?, _ value: A) {
if let err = error {
self = .Error(err)
} else {
self = .Value(Box(value))
}
}
}
現(xiàn)在我們可以把所有的函數(shù)使用綁定運(yùn)算符連接到一起了:
func getUser(request: NSURLRequest, callback: (Result<User>) -> ()) {
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, urlResponse, error in
let responseResult = Result(error,
Response(data: data, urlResponse: urlResponse))
let result = responseResult >>> parseResponse
>>> decodeJSON
>>> User.decode
callback(result)
}
task.resume()
}
Wow,即使再次書寫這些代碼,我都對(duì)這些結(jié)果感到興奮。你可能會(huì)想,"這已經(jīng)非??犰帕?,我們已經(jīng)迫不及待的想用它了!",但是這還不算完!
重構(gòu):使用泛型抽象類型
已經(jīng)非常棒了,但是我們?nèi)匀幌刖帉戇@個(gè)解析器適用于任何類型。我可以使用泛型(Generics)來(lái)使得解析器完全抽象。
我們引入JSONDecodable協(xié)議,讓上面的類型遵守它。協(xié)議看起來(lái)是這樣的:
protocol JSONDecodable {
class func decode(json: JSON) -> Self?
}
然后,我們編寫一個(gè)函數(shù),解析任何遵守JSONDecodable協(xié)議的類型為Result類型:
func decodeObject<A: JSONDecodable>(json: JSON) -> Result<A> {
return resultFromOptional(A.decode(json), NSError()) // 自定義錯(cuò)誤
}
現(xiàn)在我們可以讓User遵守協(xié)議:
struct User: JSONDecodable {
let id: Int
let name: String
let email: String
static func create(id: Int)(name: String)(email: String) -> User {
return User(id: id, name: name, email: email)
}
static func decode(json: JSON) -> User? {
return JSONObject(json) >>> { d in
User.create <^>
d["id"] >>> JSONInt <*>
d["name"] >>> JSONString <*>
d["email"] >>> JSONString
}
}
我們改變了User的解析函數(shù),用可選的User替換掉Result<User>。這樣我們就擁有了一個(gè)抽象的函數(shù),可以在解碼后調(diào)用resultFromOptional,替代之前模型中必須使用的decode函數(shù)。
最后,我們抽象performRequest函數(shù)中的解析和解碼過(guò)程,讓它們變得更加易讀。下面是最終的performRequest和parseResult函數(shù):
func performRequest<A: JSONDecodable>(request: NSURLRequest, callback: (Result<A>) -> ()) {
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, urlResponse, error in
callback(parseResult(data, urlResponse, error))
}
task.resume()
}
func parseResult<A: JSONDecodable>(data: NSData!, urlResponse: NSURLResponse!, error: NSError!) -> Result<A> {
let responseResult = Result(error, Response(data: data, urlResponse: urlResponse))
return responseResult >>> parseResponse
>>> decodeJSON
>>> decodeObject
}
繼續(xù)學(xué)習(xí)
實(shí)例代碼放在了GitHub上供下載
如果你對(duì)函數(shù)式編程或者這篇文章討論的任何概念感興趣,請(qǐng)查閱Haskell編程語(yǔ)言和Learn You a Haskell書中的一篇特定文章,同時(shí),請(qǐng)查閱Pat Brisbin寫的博客:Applicative Options Parsing in Haskell