1.Эффективный JSON с функциональными концепциями и дженериками в Swift

Это перевод первой статьи  Efficient JSON in Swift with Functional Concepts and Generics, написанной Tony DiPasquale

Несколько месяцев назад Apple представила новый язык программирования, Swift, чем сильно воодушевила разработчиков относительно будущего написания приложений для iOS и OS X. Люди немедленно, начиная с версии  Xcode 6 Beta1 начали пробовать Swift и понадобилось не так много времени, чтобы обнаружить, что парсинг JSON – редкое приложение обходится без него – не так прост как в Objective-C. Swift является статически типизованным языком, а это означает, что мы не можем больше забрасывать объекты в типизованные переменные и заставлять компилятор доверять нам, что они таковыми и являются. Теперь, в Swift, компилятор выполняет проверку, давая нам уверенность, что мы случайно не вызовем runtime ошибки. Это позволяет нам опираться на компилятор при создании безошибочного кода, но это также означает, что мы должны делать дополнительную работу, чтобы его удовлетворить. В этом посту я обсуждаю API для парсинга JSON, который использует концепции функционального программирования и дженерики ( Generics ) для создания читаемого и эффективного кода.

Запрашиваем  Модель  User

Первое, что нам необходимо – это способ преобразования данных, которые мы получаем по сетевому запросу, в  JSON. В прошлом мы использовали NSJSONSerialization.JSONObjectWithData(NSData, Int, &NSError), что давало нам тип данных Optional JSON и возможную ошибку (error), если возникали проблемы с парсингом. Тип данных для JSON объектов в  Objective-C – это 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) -&gt; ()) {
  let task = NSURLSession.sharedSession().dataTaskWithRequest(request)
         { data, urlResponse, error in
                  var jsonErrorOptional: NSError?
                  let jsonOptional: AnyObject! =
                         NSJSONSerialization.JSONObjectWithData(data,
                                  options: NSJSONReadingOptions(0),
                                    error: &amp;jsonErrorOptional)

    if let json = jsonOptional as? Dictionary&lt;String, AnyObject&gt; {
      if let id = json[&quot;id&quot;] as AnyObject? as? Int {
        if let name = json[&quot;name&quot;] as AnyObject? as? String {
          if let email = json[&quot;email&quot;] as AnyObject? as? String {
            let user = User(id: id, name: name, email: email)
            callback(user)
          }
        }
      }
    }
  }
  task.resume()
}

После многочисленных вложенных if-let предложений, мы наконец-то получили наш User объект. Можно себе представить, что  чем больше у модели будет свойств, тем она будет выглядеть все ужаснее и ужаснее . Кроме того, мы не отслеживаем ошибки, которые возможны на любом шаге: в случае ошибки мы не получим ничего. Наконец, мы должны будем писать этот код для каждой модели, которая требуется для нашего  API, что приведет к значительному дублированию кода.
Начнем рефакторинг нашего кода, но прежде для упрощения JSON типов определим некоторые алиасы типов typealias .

typealias JSON = AnyObject
typealias JSONDictionary = Dictionary&lt;String, JSON&gt;
typealias JSONArray = Array&lt;JSON&gt;

Рефакторинг: Добавляем управления ошибками (Error Handling)

Во-первых, мы будем проводить рефакторинг нашей функции с целью управления ошибками.

И тут нам понадобится первая концепция функционального программирования, тип Either<A, B>. Это позволит нам вернуть пользователю объект как в случае, если все проходит успешно, так и в случае возникновения ошибки. В Swift можно так реализовать тип Either<A, B> :

enum Either&lt;A, B&gt; {
  case Left(A)
  case Right(B)
}

Мы можем использовать Either<NSError, User> в качестве типа, который передается нашему callback, следовательно, вызывающая функция сможет управлять как успешно “разобранной” моделью User, так и ошибкой (error).

func getUser(request: NSURLRequest, callback: (Either&lt;NSError, User&gt;) -&gt; ()) {
  let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, urlResponse, error in
    // если возвращается ошибка, то посылаем ее в callback
    if let err = error {
      callback(.Left(err))
      return
    }

    var jsonErrorOptional: NSError?
    let jsonOptional: JSON! = NSJSONSerialization.JSONObjectWithData(data,
                                                options: NSJSONReadingOptions(0),
                                                  error: &amp;jsonErrorOptional)

    // если возникает ошибка парсирга JSON, посылаем ее в callback
    if let err = jsonErrorOptional {
      callback(.Left(err))
      return
    }

    if let json = jsonOptional as? JSONDictionary {
      if let id = json[&quot;id&quot;] as AnyObject? as? Int {
        if let name = json[&quot;name&quot;] as AnyObject? as? String {
          if let email = json[&quot;email&quot;] as AnyObject? as? String {
            let user = User(id: id, name: name, email: email)
            callback(.Right(user))
            return
          }
        }
      }
    }

    // если не смогли "разобрать" какие-то свойства, то посылаем ошибку в callback
    callback(.Left(NSError()))
  }
  task.resume()
}

Теперь функция, вызывающая наш getUser может использовать switch предложение для Either, и что-то делать с текущим пользователем User или показывать ошибку.

getUser(request) { either in
  switch either {
  case let .Left(error):
    // показываем сообщение об ошибке

  case let .Right(user):
    // делаем что-то с user
  }
}

Мы немного упростили это, предполагая, что Left всегда будет NSError. Вместо этого давайте использовать подобный, но другой тип Result <A> , который будет содержать либо значение, которое мы ищем, либо ошибку. Его реализация выглядит так:

enum Result&lt;A&gt; {
  case Error(NSError)
  case Value(A)
}

В текущей версии Swift (1.1), тип Result <A> вызовет ошибку компиляции. Swift должен знать, какой тип будет помещен внутрь всех значений перечисления (enum). Мы можем создать постоянный класс (constant class) для размещения нашего generic значения A.

(Примечание переводчика. В настоящий момент в Swift перечисления enum не могут быть дженериками (generic) на самом топовом уровне, но,  как было сказано в статье, могут быть представлены как generic, если их обернуть в “постоянный” class  box):

final class Box&lt;A&gt; {
  let value: A

  init(_ value: A) {
    self.value = value
  }
}

enum Result&lt;A&gt; {
  case Error(NSError)
  case Value(Box&lt;A&gt;)
}

Заменяя Either на Result, мы получим следующее:

func getUser(request: NSURLRequest, callback: (Result&lt;User&gt;) -&gt; ()) {
  let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, urlResponse, error in
    // если возвращается ошибка, то посылаем ее в callback
    if let err = error {
      callback(.Error(err))
      return
    }

    var jsonErrorOptional: NSError?
    let jsonOptional: JSON! = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(0), error: &amp;jsonErrorOptional)

    //  если возникает ошибка парсирга JSON, посылаем ее в callback
    if let err = jsonErrorOptional {
      callback(.Error(err))
      return
    }

    if let json = jsonOptional as? JSONDictionary {
      if let id = json[&quot;id&quot;] as AnyObject? as? Int {
        if let name = json[&quot;name&quot;] as AnyObject? as? String {
          if let email = json[&quot;email&quot;] as AnyObject? as? String {
            let user = User(id: id, name: name, email: email)
            callback(.Value(Box(user)))
            return
          }
        }
      }
    }

    // если не смогли "разобрать" какие-то свойства, то посылаем ошибку в callback
    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, так что необходимы 3 функции для парсинга этих типов.

func JSONString(object: JSON?) -&gt; String? {
  return object as? String
}

func JSONInt(object: JSON?) -&gt; Int? {
  return object as? Int
}

func JSONObject(object: JSON?) -&gt; JSONDictionary? {
  return object as? JSONDictionary
}

Теперь JSON парсинг выглядит так:

if let json = JSONObject(jsonOptional) {
  if let id = JSONInt(json[&quot;id&quot;]) {
    if let name = JSONString(json[&quot;name&quot;]) {
      if let email = JSONString(json[&quot;email&quot;]) {
        let user = User(id: id, name: name, email: email)
      }
    }
  }
}

Использование этих функций все еще не отменяет кучу  if-let синтаксиса. Такие концепции функционального программирования как МонадыФункторы, Аппликативные Функторы и Каррирование (Currying) помогут нам “сжать” наш парсинг.

Во-первых, давайте посмотрим  на монаду Maybe, которая похожа на Optionals в  Swift. У монад есть оператор bind (“связывание”), который, при использовании с Optionals, разрешает нам “связывать” Optional c функцией, которая берет non-Optional и возвращает Optional. Если первый Optional, который на входе,- это  .None, то возвращается .None, в противном случае оператор bind “разворачивает” первый Optional и применяет к нему функцию.

infix operator &gt;&gt;&gt; { associativity left precedence 150 }

func &gt;&gt;&gt;&lt;A, B&gt;(a: A?, f: A -&gt; B?) -&gt; B? {
  if let x = a {
    return f(x)
  } else {
    return .None
  }
}

В других функциональных языках оператор  >>= используется для bind (“связывания”); но в Swift этот оператор уже занят и используется для побитового сдвига, так что вместо него будем использовать оператор  >>> . Применяя его к JSON парсингу, получим:

if let json = jsonOptional &gt;&gt;&gt; JSONObject {
  if let id = json[&quot;id&quot;] &gt;&gt;&gt; JSONInt {
    if let name = json[&quot;name&quot;] &gt;&gt;&gt; JSONString {
      if let email = json[&quot;email&quot;] &gt;&gt;&gt; JSONString {
        let user = User(id: id, name: name, email: email)
      }
    }
  }
}

Теперь мы можем убрать Optional параметры из наших парсеров:

func JSONString(object: JSON) -&gt; String? {
  return object as? String
}

func JSONInt(object: JSON) -&gt; Int? {
  return object as? Int
}

func JSONObject(object: JSON) -&gt; JSONDictionary? {
  return object as? JSONDictionary
}

У функторов (Functors) есть оператор fmap для применения функций к значениям, обернутым в некоторый контекст. У аппликативных функторов (Applicative Functors) также есть оператор apply для применения обернутых функций к значениям, обернутым  в некоторый контекст. В нашем случае контекст, в который “заворачиваются” наши значения – это Optional. Это означает, что мы можем комбинировать многочисленные Optional значения с функцией, которая берет множество non-Optional значений. Если все значения присутствуют и представлены .Some, то мы получаем результат, обернутый в Optional. Если какое-то из этих значений представлено  .None, мы получаем .None. Мы можем определить эти операторы в Swift следующим образом:

infix operator &lt;^&gt; { associativity left } // Functor's fmap (usually &lt;$&gt;)
infix operator &lt;*&gt; { associativity left } // Applicative's apply

func &lt;^&gt;&lt;A, B&gt;(f: A -&gt; B, a: A?) -&gt; B? {
  if let x = a {
    return f(x)
  } else {
    return .None
  }
}

func &lt;*&gt;&lt;A, B&gt;(f: (A -&gt; B)?, a: A?) -&gt; B? {
  if let x = a {
    if let fx = f {
      return fx(x)
    }
  }
  return .None
}

Но прежде, чем мы соберем все это вместе, нам необходимо каррировать вручную инициализатор (init) нашей модели User, так как Swift не поддерживает автокаррирование (auto-currying). Каррирование (currying) означает, что если мы на вход каррирования подаем функцию с меньшим числом параметров, чем у нее есть, то каррирование возвращает функцию с оставшимися параметрами. И наша User модель будет выглядеть так:

struct User {
  let id: Int
  let name: String
  let email: String

  static func create(id: Int)(name: String)(email: String) -&gt; User {
    return User(id: id, name: name, email: email)
  }
}

Собирая все вместе, наш JSON парсинг будет выглядеть так:

if let json = jsonOptional &gt;&gt;&gt; JSONObject {
  let user = User.create &lt;^&gt;
              json[&quot;id&quot;]    &gt;&gt;&gt; JSONInt    &lt;*&gt;
              json[&quot;name&quot;]  &gt;&gt;&gt; JSONString &lt;*&gt;
              json[&quot;email&quot;] &gt;&gt;&gt; JSONString
}

Если какой-то из наших парсеров возвращает .None, то user будет .None. Это выглядит намного лучше, но мы еще не закончили.
Теперь наша функция getUser изменится:

func getUser(request: NSURLRequest, callback: (Result&lt;User&gt;) -&gt; ()) {
  let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, urlResponse, error in
    // если возвращается ошибка, то посылаем ее в callback
    if let err = error {
      callback(.Error(err))
      return
    }

    var jsonErrorOptional: NSError?
    let jsonOptional: JSON! = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(0), error: &amp;jsonErrorOptional)

    // если возникает ощибка парсинга JSON, посылаем ее в callback
    if let err = jsonErrorOptional {
      callback(.Error(err))
      return
    }

    if let json = jsonOptional &gt;&gt;&gt; JSONObject {
      let user = User.create &lt;^&gt;
                  json[&quot;id&quot;]    &gt;&gt;&gt; JSONInt    &lt;*&gt;
                  json[&quot;name&quot;]  &gt;&gt;&gt; JSONString &lt;*&gt;
                  json[&quot;email&quot;] &gt;&gt;&gt; JSONString
      if let u = user {
        callback(.Value(Box(u)))
        return
      }
    }

    //  если не смогли "разобрать" какие-то свойства, то посылаем ошибку в callback
    callback(.Error(NSError()))
  }
  task.resume()
}

Рефакторинг: Убираем многочисленные returns с помощью “связывания” (bind)

Заметьте, что в предыдущей функции мы четыре раза вызываем callback. Если мы забудем хотя бы одно предложение return, то мы ошибочно представим результат как NSError. Мы можем уничтожить этот потенциальный bug и сделать более понятной эту функцию в дальнейшем, если разобьем эту функцию на 3 различные части: парсинг ответа сервера, парсинг данных в JSON и парсинг  JSON в объект User. Каждый из этих шагов берет один вход и возвращает результат его преобразования для следующего шага или ошибку (error). Это звучит как идеальный случай для использования оператора bind (“связывания”) для нашего типа  Result.

Функции parseResponse понадобится  Result с data и статусным кодом (status code) ответа сервера.  API  iOS дает нам только NSURLResponse и держит data отдельно, поэтому мы сделаем маленькую вспомогательную структуру  :

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
    }
  }
}

Теперь мы можем передать нашей функции parseResponse структуру Response и проверить ответ сервера на ошибки, прежде чем заниматься с data.

func parseResponse(response: Response) -&gt; Result&lt;NSData&gt; {
  let successRange = 200..&lt;300
  if !contains(successRange, response.statusCode) {
    return .Error(NSError()) // настройте сообщение об ошибке как вам нравится
  }
  return .Value(Box(response.data))
}

Следующие функции понадобятся нам для преобразования типа Optional в тип Result, но прежде создадим одну очень простую абстракцию.

func resultFromOptional&lt;A&gt;(optional: A?, error: NSError) -&gt; Result&lt;A&gt; {
  if let a = optional {
    return .Value(Box(a))
  } else {
    return .Error(error)
  }
}

Следующая функция – преобразование наших data в JSON:

func decodeJSON(data: NSData) -&gt; Result&lt;JSON&gt; {
  let jsonOptional: JSON! = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(0), error: &amp;jsonErrorOptional)
  return resultFromOptional(jsonOptional, NSError()) // используйте ошибку из NSJSONSerialization или задайте свое сообщение об ошибке
}

Теперь добавляем декодирование JSON непосредственно в саму модель:

struct User {
  let id: Int
  let name: String
  let email: String

  static func create(id: Int)(name: String)(email: String) -&gt; User {
    return User(id: id, name: name, email: email)
  }

  static func decode(json: JSON) -&gt; Result&lt;User&gt; {
    let user = JSONObject(json) &gt;&gt;&gt; { dict in
      User.create &lt;^&gt;
          dict[&quot;id&quot;]    &gt;&gt;&gt; JSONInt    &lt;*&gt;
          dict[&quot;name&quot;]  &gt;&gt;&gt; JSONString &lt;*&gt;
          dict[&quot;email&quot;] &gt;&gt;&gt; JSONString
    }
    return resultFromOptional(user, NSError()) // задайте сообщение об ошибке
  }
}

Перед тем, как скомбинировать все вместе, давайте распространим оператора >>>, на тип  Result:

func &gt;&gt;&gt;&lt;A, B&gt;(a: Result&lt;A&gt;, f: A -&gt; Result&lt;B&gt;) -&gt; Result&lt;B&gt; {
  switch a {
  case let .Value(x):     return f(x.value)
  case let .Error(error): return .Error(error)
  }
}

И добавим пользовательский инициализатор к Result:

enum Result&lt;A&gt; {
  case Error(NSError)
  case Value(Box&lt;A&gt;)

  init(_ error: NSError?, _ value: A) {
    if let err = error {
      self = .Error(err)
    } else {
      self = .Value(Box(value))
    }
  }
}

Теперь комбинируем все эти функции с оператором >>>.

func getUser(request: NSURLRequest, callback: (Result&lt;User&gt;) -&gt; ()) {
  let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, urlResponse, error in
    let responseResult = Result(error, Response(data: data, urlResponse: urlResponse))
    let result = responseResult &gt;&gt;&gt; parseResponse
                                &gt;&gt;&gt; decodeJSON
                                &gt;&gt;&gt; User.decode
    callback(result)
  }
  task.resume()
}

Здорово, я восхищен полученным результатом. Вы можете подумать: “Это действительно круто. Не могу дождаться чтобы это попробовать!”, но нам нужно еще кое-что доделать!

Рефакторинг:  Избавляемся от “типа” с помощью дженериков (generics)

Это здорово, но придется писать это для каждой модели, которую мы хотим получать из JSON. Давайте использовать дженерики ( generics), чтобы сделать это абсолютно абстрактным.
Мы введем протокол JSONDecodable и скажем нашей функции, что возвращаемый тип должен подтверждать этот протокол :

protocol JSONDecodable {
class func decode(json: JSON) -&gt; Self?
}

Следующим шагом мы напишем функцию, которая будет декодировать любую модель, подтверждающую протокол JSONDecodable, в  Result:

func decodeObject&lt;A: JSONDecodable&gt;(json: JSON) -&gt; Result&lt;A&gt; {
  return resultFromOptional(A.decode(json), NSError()) // custom error
}

Теперь заставим  User подтвердить этот протокол:

struct User: JSONDecodable {
  let id: Int
  let name: String
  let email: String

  static func create(id: Int)(name: String)(email: String) -&gt; User {
    return User(id: id, name: name, email: email)
  }

  static func decode(json: JSON) -&gt; User? {
    return JSONObject(json) &gt;&gt;&gt; { d in
      User.create &lt;^&gt;
        json[&quot;id&quot;]    &gt;&gt;&gt; JSONInt    &lt;*&gt;
        json[&quot;name&quot;]  &gt;&gt;&gt; JSONString &lt;*&gt;
        json[&quot;email&quot;] &gt;&gt;&gt; JSONString
  }
}

Мы изменили функцию декодера decode для  User так, чтобы она возвращала Optional User вместо Result<User>. Это позволяет нам иметь абстрактную функцию, которая вызывает resultFromOptional после decode вместо того, чтобы вызывать ее для каждой модели в функции decode.
Наконец, для лучшей читаемости уберем парсинг и декодирование из функции performRequest. Теперь у нас есть две финальные функции:  performRequest и parseResult:

func performRequest&lt;A: JSONDecodable&gt;(request: NSURLRequest, callback: (Result&lt;A&gt;) -&gt; ()) {
  let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, urlResponse, error in
    callback(parseResult(data, urlResponse, error))
  }
  task.resume()
}

func parseResult&lt;A: JSONDecodable&gt;(data: NSData!, urlResponse: NSURLResponse!, error: NSError!) -&gt; Result&lt;A&gt; {
  let responseResult = Result(error, Response(data: data, urlResponse: urlResponse))
  return responseResult &gt;&gt;&gt; parseResponse
                        &gt;&gt;&gt; decodeJSON
                        &gt;&gt;&gt; decodeObject
}

Дальнейшее изучение

Пример кода представлен в GitHub.

Если вы интересуетесь функциональным программированием или какими-то его аспектами, представленными в этом посте, посмотрите  Haskell и особенно  this post из книги Learn You a Haskell. Также посмотрите пост Pat Brisbin’s post о парсинге options с использованием аппликативного функтора.

Leave a Reply

Your email address will not be published. Required fields are marked *