使用泛型与函数式思想高效解析 JSON

更多优秀译文请关注我们的微信公众号: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 的值有可能为 StringDoubleBoolArrayDictionary 或者 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, IntDictionary 三种类型,所以我们需要三个函数来对这三种类型进行解析。

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 语句。函数式编程中的 MonadsApplicative 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函数中的解析和解码过程,让它们变得更加易读。下面是最终的performRequestparseResult函数:

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • 生活就像一块巧克力,你永远不知道它到底有多黑。 生活如一片死水,她出生了,上学了,被欺负了,又在众多否定的声音中考...
    优票票阅读 125评论 0 0
  • 蓦然回首三十载, 一事无成心自怜。 雄心犹在已无志, 兀自空想花已残。 人生匆忙至而立,猛然回首事无成; 呆坐空想...
    闲居散人阅读 794评论 0 5
  • 风起龙飞阅读 213评论 0 13