Эффективный JSON с функциональными концепциями и generics в Swift

в 10:08, , рубрики: functional programming, generics, ios 8, mobile development, swift, вывод типов, Программирование, разработка под iOS, функциональное программирование

Это перевод статьи Tony DiPasquale «Efficient JSON in Swift with Functional Concepts».

Предисловие переводчика.

Передо мной была поставлена задача: закачать данные в формате JSON с Flickr.com о 100 топ местах, в которых сделаны фотографии на данный момент, в массив моделей:

//------ Массив моделей Places
struct Places {   
    var places : [Place]
}

//-----Модель Place
struct Place {

    let placeURL: NSURL
    let timeZone: String
    let photoCount : Int
    let content : String
    
}

Кроме чисто прагматической задачи, мне хотелось посмотреть как в Swift работает «вывод типа из контекста» (type Inference), какие возможности Swift в функциональном программировании, и я выбрала для парсинга JSON алгоритмы из статьи Tony DiPasquale «Efficient JSON in Swift with Functional Concepts and Generics», в которой он «протягивает» generic тип Result<A> для обработки ошибок по всей цепочке преобразований: от запроса в сеть до размещения данных в массив Моделей для последующего представления в UITableViewController.
Чтобы посмотреть как Swift работает «в связке» с Objective-C, для считывания данных с Flickr.com использовался Flickr API, представленный в курсе Стэнфордского Университета «Stanford CS 193P iOS 7», написанный на Objective-C.
В результате помимо небольшого расширения Моделей:

extension Place: JSONDecodable {
    static func create(placeURL: String)(timeZone: String)(photoCount: String)(content: String) -> Place {
        return Place(placeURL: toURL(placeURL), timeZone: timeZone, photoCount: photoCount.toInt() ?? 0, content: content)
    }
    static func decode(json: JSON) -> Place? {
        return _JSONParse(json) >>> { d in
            Place.create
                <^> d <| "place_url"
                <*> d <| "timezone"
                <*> d <| "photo_count"
                <*> d <| "_content"
        }
    }
}

extension Places: JSONDecodable { 
    static func create(places: [Place]) -> Places {
        return Places(places: places)
    }
    static func decode(json: JSON) -> Places? {
        return _JSONParse(json) >>> { d in
            Places.create
                <^> d <| "places" <| "place"
            
        }
    }
}

мне самостоятельно пришлось написать только три строчки кода:

class ViewController: UIViewController {
        override func viewDidLoad() {
        super.viewDidLoad()
        
        //--------------- URL для places из Flickr.com ------------------------------------------      
        let urlPlaces  = NSURLRequest( URL: FlickrFetcher.URLforTopPlaces())       
        performRequest(urlPlaces ) { (places: Result<Places>) in
            println("(stringResult(places))")                      
        }
    }
}

и добавить «мост» между Swift и Objective-C — файл EfficientJSONBrief-Bridging-Header.h, в котором указываем FlickrFetcher.h файл API Flickr:

Эффективный JSON с функциональными концепциями и generics в Swift - 1

Код можно посмотреть на Github.

Перевод.

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

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

Первое, что нам необходимо — это преобразование данных, которые мы получаем по сетевому запросу, в JSON. В прошлом мы использовали NSJSONSerialization.JSONObjectWithData(NSData, Int, &NSError), что давало нам тип данных Optional JSON и возможную ошибку ( error ), если возникали проблемы с парсингом. Тип данных для JSON объектов в Objective-C — это NSDictionary, который может содержать любые объекты в своих values (значениях). В 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 {
        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 .

typealias JSON = AnyObject
typealias JSONDictionary = Dictionary<String, JSON>
typealias JSONArray = Array<JSON>

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

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

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

enum Either<A, B> {
  case Left(A)
  case Right(B)
}

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

func getUser(request: NSURLRequest, callback: (Either<NSError, User>) -> ()) {
  let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, urlResponse, error in
    // if the response returned an error send it to the callback
    if let err = error {
      callback(.Left(err))
      return
    }

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

    // if there was an error parsing the JSON send it back
    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
          }
        }
      }
    }

    // if we couldn't parse all the properties then send back an error
    callback(.Left(NSError()))
  }
  task.resume()
}

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

getUser(request) { either in
  switch either {
  case let .Left(error):
    // display error message

  case let .Right(user):
    // do something with user
  }
}

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

enum Result<A> {
  case Error(NSError)
  case Value(A)
}

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

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

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 the response returned an error send it to the callback
    if let err = error {
      callback(.Error(err))
      return
    }

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

    // if there was an error parsing the JSON send it back
    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
          }
        }
      }
    }

    // if we couldn't parse all the properties then send back an error
    callback(.Error(NSError()))
  }
  task.resume()
}

getUser(request) { result in
  switch result {
  case let .Error(error):
    // display error message

  case let .Value(boxedUser):
    let user = boxedUser.value
    // do something with user
  }
}

Небольшое изменение. Но давайте продолжим рефакторинг.

Рефакторинг: Уничтожение дерева проверки типов

На следующем этапе мы избавимся от уродливых JSON парсингов путем создания отдельных JSON парсеров для каждого типа. В нашем объекте есть только String, Int и Dictionary, так что необходимы 3 функции для парсинга этих типов.

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) помогут нам «сжать» наш парсинг.

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

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

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

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 для применения «завернутых» функций к значениям, «завернутым» в некоторый контекст. В нашем случае контекст, в который «заворачиваются» наши значения — это Optional. Это означает, что мы можем комбинировать многочисленные Optional значения с функцией, которая берет множество non-Optional значений. Если все значения присутствуют и представлены .Some, то мы получаем результат, «завернутый» в Optional. Если какое-то из этих значений представлено .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
}

Но прежде, чем мы соберем все это вместе, нам необходимо каррировать вручную инициализатор (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) -> 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 the response returned an error send it to the callback
    if let err = error {
      callback(.Error(err))
      return
    }

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

    // if there was an error parsing the JSON send it back
    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
      }
    }

    // if we couldn't parse all the properties then send back an error
    callback(.Error(NSError()))
  }
  task.resume()
}

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

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

Функции parseResponse понадобится Result с data и статусным кодом (status code) ответа из сети (response). 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) -> Result<NSData> {
  let successRange = 200..<300
  if !contains(successRange, response.statusCode) {
    return .Error(NSError()) // customize the error message to your liking
  }
  return .Value(Box(response.data))
}

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

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

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

func decodeJSON(data: NSData) -> Result<JSON> {
  let jsonOptional: JSON! = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(0), error: &jsonErrorOptional)
  return resultFromOptional(jsonOptional, NSError()) // use the error from NSJSONSerialization or a custom error message
}

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

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()) // custom error message
  }
}

Перед тем, как скомбинировать все вместе, давайте распространим оператор «bind» (связывание), >>>, на тип 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)
  }
}

И добавляем пользовательский (custom) инициализатор к 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))
    }
  }
}

Теперь комбинируем все эти функции с оператором «bind» (связывания).

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

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

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

Это здорово, но мы все еще должны написать это для каждой модели, которую мы хотим получить из JSON. Мы можем использовать дженерики ( 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()) // custom error
}

Теперь заставим 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 <^>
        json["id"]    >>> JSONInt    <*>
        json["name"]  >>> JSONString <*>
        json["email"] >>> JSONString
  }
}

Мы изменили функцию декодера decode для User так, чтобы она возвращала Optional User вместо Result<User>. Это позволяет нам иметь абстрактную функцию, которая вызывает resultFromOptional после decode вместо того, чтобы вызывать ее для каждой модели в функции 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 и особенно this post из книги Learn You a Haskell. Также посмотрите пост Pat Brisbin’s post об options парсинге, используя Applicative.

Послесловие переводчика

В ссылке GitHub дан лишь окончательный вариант кода. Если вы хотите следовать за логикой статьи шаг за шагом, а также тестировать алгоритмы, представленные в статье, на данных, имитирующих ошибки, то код можно найти здесь.

Автор: WildGreyPlus

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js