2.C.Реальный мир парсинга JSON в Swift

Это перевод второй статьи  Real World JSON Parsing with Swift, написанной Tony DiPasquale.

В прошлом посте (соответствующий русский перевод) , мы рассмотрели использование концепций функционального программирования и generics для парсинга полученного с сервера JSON непосредственно в  User Модель. Окончательный результат JSON парсинга выглядел так:

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

Здорово, но в реальном мире объекты, которые мы получаем с сервера через  API, не всегда могут оказаться идеальными. Иногда объект может иметь только несколько важных ключей, а остальные восстанавливаются позже.
Например, если мы получаем с сервера текущего пользователя, то мы хотим знать о нем всю информацию , но если мы получаем пользователей по их id нам не нужен email по соображениям секретности. Скрывая email, сервер вернет только id и name для всех пользователей, которые не являются текущим. Для использования одного и того же объекта User в разнообразных ситуациях, более реалистично описать User так:

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

Вы видите, что  тип свойства email  – это Optional String. Если вы помните из предыдущего поста, то операторы <^> (fmap) и <*> (apply) обеспечивают получение структуры User только в случае присутствия в JSON всех ключей; в противном случае мы получаем .None. Если с сервера email возвратилась как .None или nil, то функция decode даст ошибку. Мы можем это исправить путем добавления функции pure.

pure – это функция, которая берет значение без контекста и помещает его в минимальный контекст. Swift Optionals – это значения в контексте “есть или нет”; следовательно, pure означает .Some(value). Реализация очень простая:

func pure<A>(a: A) -> A? {
  return .Some(a)
}

Теперь мы можем использовать эту функцию для парсинга User , который может иметь или не иметь свойство email:

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  <*>
        pure(d["email"]       >>> _JSONString)
    }
  }
}

Без функции  pure  d["email"] >>> _JSONString вернула бы .None при отсутствии ключа "email" в словаре d. Оператор <*>  ищет Optional, и если видит .None, то останавливает создание User и возвращает .None для всей функции create. Однако, вызывая pure  при отсутствии email,  мы получим .Some (.None) и оператор <*> “развернет” этот Optional,  и передаст  .None  инициализатору.

Больше использовать “вывод типов” ( inference )

Вышеприведенный код прекрасно работает, но все еще слишком много синтаксиса, который мы должны написать для получения нашего User. Сделаем рефакторинг, привлекая механизм “вывода типов” ( inference ), действующий  в Swift.

Мы уже используем в процессе парсинга JSON несколько функций для преобразования типа AnyObject в тип, который требуется нашей функции create. Имея ввиду “вывод типов”, мы можем явно указать функции create какой тип мы ищем. К настоящему моменту ,_JSONInt и _JSONString выглядят так:

func _JSONInt(json: JSON) -> Int? {
  return json as? Int
}

func _JSONString(json: JSON) -> String? {
  return json as? String
}

Легко видеть, что эти функции очень похожи, фактически они отличаются только типом. И это повод, чтобы использовать  generics.

func _JSONParse<A>(json: JSON) -> A? {
  return json as? A
}

Теперь мы можем использовать _JSONParse в нашей decode функции вместо специальной функции для парсинга каждого отдельного JSON типа.

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 _JSONParse(json) >>> { (d: JSONObject) in
      User.create <^>
             d["id"]          >>> _JSONParse  <*>
             d["name"]        >>> _JSONParse  <*>
        pure(d["email"]       >>> _JSONParse)
    }
  }
}

Это работает потому, что User.create –  функция, которая берет Int и применяет его к d["id"] >>> _JSONParse. Компилятор будет “выводить тип” Int для _JSONParse.
Необходимо отметить, что для того, чтобы использовать _JSONParse вместо  _JSONObject нам пришлось сделать “кастинг” d как JSONObject, так чтобы  _JSONParse смогла “вывести” правильный тип.
Наша декодирующая функция становится все лучше и лучше, но остается еще много дублирования. Было бы неплохо уничтожить все вызовы _JSONParse. Эти строки все одинаковые за исключением ключей, используемых для извлечения JSON значений. Используем абстракцию для удаления дублирования. То же  можно сделать для  любого значения, которому понадобится вызвать  pure.

func extract<a>(json: JSONObject, key: String) -> A? {
return json[key] >>> _JSONParse
}</a>

func extractPure<a>(json: JSONObject, key: String) -> A?? {
return pure(json[key] >>> _JSONParse)
}

В результате получаем:

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 _JSONParse(json) >>> { d in
      User.create <^>
        extract(d, "id")        <*>
        extract(d, "name")      <*>
        extractPure(d, "email")
    }
  }
}

Теперь, когда у нас есть функция extract, которая берет JSONObject в качестве первого параметра, мы можем убрать “кастинг”  для d, так как для него сработает “вывод типа” при передачи в extract.

Для extract и extractPure придется многократно набирать соответствующий текст, и создание infix оператора улучшило бы читаемость кода. Давайте создадим пару пользовательских операторов для этого. Будем использовать <| и <|?, которые “навеяны” популярной Haskell библиотекой Aeson для парсинга JSON. Aeson использует .: и .:?, но это нелегальные операторы в  Swift, так что вместо этого мы будем использовать  <| версию.

infix operator <|  { associativity left precedence 150 }
infix operator <|? { associativity left precedence 150 }

func <|<A>(json: JSONObject, key: String) -> A? {
  return json[key] >>> _JSONParse
}

func <|?<A>(json: JSONObject, key: String) -> A?? {
  return pure(json[key] >>> _JSONParse)
}

Эти операторы мы можем использовать в декодировании нашего  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 _JSONParse(json) >>> { d in
      User.create
        <^> d <|  "id"
        <*> d <|  "name"
        <*> d <|? "email"
    }
  }
}

Вот это да! Используя дженерики (generics), и полагаясь на “вывод типов”  в Swift, мы можем реально уменьшить количество кода, который нам нужно написать. Но что особенно интересно, это как близко мы подошли к такому чисто функциональному языку программирования как Haskell. При использовании Aeson в  Haskell, декодирование User выглядело бы так:

instance FromJSON User where
  parseJSON (Object o) = User
    <$> o .:  "id"
    <*> o .:  "name"
    <*> o .:? "email"
  parseJSON _ = mzero

Заключение

Мы прошли длинный путь от нашего первого поста. Давайте вернемся назад и сравним как выглядел парсинг User тогда и сейчас, исключая преобразование  NSData в AnyObject?.

Первоначальный метод

extension User {
  static func decode(json: AnyObject) -> User? {
    if let jsonObject = json as? [String:AnyObject] {
      if let id = jsonObject["id"] as AnyObject? as? Int {
        if let name = jsonObject["name"] as AnyObject? as? String {
          if let email = jsonObject["email"] as AnyObject? {
            return User(id: id, name: name, email: email as? String)
          }
        }
      }
    }
    return .None
  }
}

Улучшенный метод

extension User: JSONDecodable {
  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 _JSONParse(json) >>> { d in
      User.create
        <^> d <|  "id"
        <*> d <|  "name"
        <*> d <|? "email"
    }
  }
}

Соответствующий код можно найти на GitHub.

Leave a Reply

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