[Перевод] Эффективный JSON с функциональными концепциями и generics в Swift
Это перевод статьи 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 для обработки ошибок по всей цепочке преобразований: от запроса в сеть до размещения данных в массив Моделей для последующего представления в 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
Код можно посмотреть на 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
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
typealias JSON = AnyObject
typealias JSONDictionary = Dictionary
И тут нам понадобится первая концепция функционального программирования, тип Either. Это позволит нам вернуть пользователю объект как в случае, если все проходит успешно, так и в случае возникновения ошибки. В Swift можно так реализовать тип Either:
func getUser (request: NSURLRequest, callback: (Either
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 , который будет содержать либо значение, которое мы ищем, либо ошибку. Его реализация выглядит так:
enum Result { case Error (NSError) case Value (A) } В текущей версии Swift (1.1), тип Result вызовет ошибку компиляции. Swift должен знать, какой тип будет помещен внутрь всех случаев (case) перечисления (enum). Мы можем создать постоянный класс (constant class) для размещения нашего generic значения A.
(Примечание переводчика. В настоящий момент в Swift перечисления enum не могут быть дженериками (generic) на самом топовом уровне, но, как было сказано в статье, могут быть представлены как generic, если их обернуть в «постоянный» class box):
final class Box { let value: A
init (_ value: A) { self.value = value } }
enum Result { case Error (NSError) case Value (Box) } Заменяя Either на Result, мы получим следующее:
func getUser (request: NSURLRequest, callback: (Result
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 }
Применяя его к 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 <^>(f: A → B, a: A?) → B? { if let x = a { return f (x) } else { return .None } }
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
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
func resultFromOptional(optional: A?, error: NSError) → Result { if let a = optional { return .Value (Box (a)) } else { return .Error (error) } } Следующая функция — преобразование наших data в JSON:
func decodeJSON (data: NSData) → Result
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
func >>>(a: Result, f: A → Result) → Result { switch a { case let .Value (x): return f (x.value) case let .Error (error): return .Error (error) } } И добавляем пользовательский (custom) инициализатор к Result:
enum Result { case Error (NSError) case Value (Box)
init (_ error: NSError?, _ value: A) { if let err = error { self = .Error (err) } else { self = .Value (Box (value)) } } } Теперь комбинируем все эти функции с оператором «bind» (связывания).
func getUser (request: NSURLRequest, callback: (Result
Рефакторинг: Избавляемся от «типа» с помощью дженериков (generics) Это здорово, но мы все еще должны написать это для каждой модели, которую мы хотим получить из JSON. Мы можем использовать дженерики (generics) чтобы сделать это абсолютно абстрактным.Мы введем протокол JSONDecodable и скажем нашей функции, что тип, который мы хотим возвращать должен подтверждать этот протокол. Этот протокол выглядит так:
protocol JSONDecodable { class func decode (json: JSON) → Self? } Следующим шагом мы напишем функцию, которая будет декодировать любую модель, подтверждающую протокол JSONDecodable, в Result:
func decodeObject
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
func performRequest
func parseResult
Пример кода представлен в GitHub.
Если вы интересуетесь функциональным программированием или какими-то его аспектами, представленными в этом посте, посмотрите Haskell и особенно this post из книги Learn You a Haskell. Также посмотрите пост Pat Brisbin«s post об options парсинге, используя Applicative.
Послесловие переводчика В ссылке GitHub дан лишь окончательный вариант кода. Если вы хотите следовать за логикой статьи шаг за шагом, а также тестировать алгоритмы, представленные в статье, на данных, имитирующих ошибки, то код можно найти здесь.