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