Upgrade to Pro — share decks privately, control downloads, hide ads and more …

JSON + Swift: Functionally Beautiful

JSON + Swift: Functionally Beautiful

An exploration into the functional way of parsing JSON in Swift. Link to download the playground: https://github.com/abijlani/json-swift

Amit Bijlani

April 10, 2015
Tweet

Other Decks in Programming

Transcript

  1. JSON Parsing var jsonError: NSError? let jsonObject: AnyObject! = NSJSONSerialization.JSONObjectWithData(data!,

    options: NSJSONReadingOptions.MutableContainers, error: &jsonError) Amit Bijlani, @paradoxed - 2015
  2. Twitter Timeline [ { "user": { "name": "OAuthDancer", "profile_sidebar_fill_color": "DDEEF6",

    "profile_background_tile": true, "profile_sidebar_border_color": "C0DEED", "created_at": "WedMar0319: 37: 35+00002010", "location": "SanFrancisco, CA", "follow_request_sent": false, "is_translator": false, "profile_link_color": "0084B4", "entities": { "url": { "urls": [ { "expanded_url": null, "url": "http: //bit.ly/oauth-dancer", "display_url": null } ] }, }, }, } ] Amit Bijlani, @paradoxed - 2015
  3. Parsing Data func parseData(data: NSData?) -> String? { var jsonError:

    NSError? let jsonObject: AnyObject! = NSJSONSerialization.JSONObjectWithData(...) if let err = jsonError { return nil } if let statuses = jsonObject as? Array<AnyObject> { if let aStatus = statuses[0] as? Dictionary<String, AnyObject> { if let user = aStatus["user"] as? Dictionary<String, AnyObject> { if let username = user["name"] { //Finally We Get The Username return username } } } } // if all else fails return nil return nil } Amit Bijlani, @paradoxed - 2015
  4. if-let Pyramid of doom! if let statuses = jsonObject as?

    Array<AnyObject> { if let aStatus = statuses[0] as? Dictionary<String, AnyObject> { if let user = aStatus["user"] as? Dictionary<String, AnyObject> { if let username = user["name"] { //Finally We Get The Username return username } } } } Amit Bijlani, @paradoxed - 2015
  5. if-let Swift 1.2 if let statuses = jsonObject as? Array<AnyObject>,

    aStatus = statuses[0] as? Dictionary<String, AnyObject>, user = aStatus["user"] as? Dictionary<String, AnyObject>, username = user["name"] as? String { //Finally We Get The Username return username } Amit Bijlani, @paradoxed - 2015
  6. Drawbacks • Large if-let statement leads to fragile code and

    legibility issues • A lot of code is repeated: • Checking for Optionals • Type conversions Amit Bijlani, @paradoxed - 2015
  7. Using Typealias typealias JSON = AnyObject typealias JSONDictionary = Dictionary<String,

    JSON> typealias JSONArray = Array<JSON> Amit Bijlani, @paradoxed - 2015
  8. Extracting Information Somewhat Cleaner if let statuses = jsonObject as?

    JSONArray, aStatus = statuses[0] as? JSONDictionary, user = aStatus["user"] as? JSONDictionary, username = user["name"] as? String { return username } Amit Bijlani, @paradoxed - 2015
  9. Error Handling func parseData(data: NSData?) -> String? { var jsonError:

    NSError? let jsonObject: AnyObject! = NSJSONSerialization.JSONObjectWithData(...) if let err = jsonError { return nil } if let statuses = jsonObject as? JSONArray, aStatus = statuses[0] as? JSONDictionary, user = aStatus["user"] as? JSONDictionary, username = user["name"] as? String { return username } // if all else fails return nil return nil } Amit Bijlani, @paradoxed - 2015
  10. Error Handling Either - Failed to parse the JSON? Or

    - Successfully parsed the JSON Amit Bijlani, @paradoxed - 2015
  11. Either Type enum Either<A, B> { case Left(A) case Right(B)

    } • Left represents failure • Right represents the correct value Amit Bijlani, @paradoxed - 2015
  12. Result Enum enum Result<A> { case Error(NSError) case Value(A) //

    causes runtime errors } Amit Bijlani, @paradoxed - 2015
  13. Container Class final class Box<A> { let value: A init(_

    value: A) { self.value = value } } enum Result<A> { case Error(NSError) case Value(Box<A>) } Amit Bijlani, @paradoxed - 2015
  14. Result Enum func parseData(data: NSData?, callback: (Result<String>) -> ()) {

    var jsonError: NSError? let jsonObject: AnyObject! = NSJSONSerialization.JSONObjectWithData(...) if let err = jsonError { callback(.Error(err)) return } if let statuses = jsonObject as? JSONArray, aStatus = statuses[0] as? JSONDictionary, user = aStatus["user"] as? JSONDictionary, username = user["name"] as? String { callback(.Value(Box(userName))) return } callback(.Error(NSError())) // pass a specific error } Amit Bijlani, @paradoxed - 2015
  15. Calling parseData // func parseData(data: NSData?, callback: (Result<String>) -> ())

    parseData(data, {(result: Result<String>) -> () in switch result { case let .Error(err): println("Error") case let .Value(userName): println(userName.value) } }) Amit Bijlani, @paradoxed - 2015
  16. Calling parseData // func parseData(data: NSData?, callback: (Result<String>) -> ())

    // Trailing closure parseData(data) {result in switch result { case let .Error(err): println("Something went wrong!") case let .Value(userName): println(userName.value.name) } } Amit Bijlani, @paradoxed - 2015
  17. Clearing up Casting if let statuses = jsonObject as? JSONArray,

    aStatus = statuses[0] as? JSONDictionary, user = aStatus["user"] as? JSONDictionary, username = user["name"] as? String { callback(.Value(Box(username))) return } Amit Bijlani, @paradoxed - 2015
  18. Creating Cast Functions func JSONToString(object: JSON?) -> String? { return

    object as? String } func JSONToInt(object: JSON?) -> Int? { return object as? Int } func JSONToDictionary(object: JSON?) -> JSONDictionary? { return object as? JSONDictionary } func JSONToArray(object: JSON?) -> JSONArray? { return object as? JSONArray } Amit Bijlani, @paradoxed - 2015
  19. Using Cast Functions if let statuses = JSONToArray(jsonObject), aStatus =

    JSONToDictionary(statuses[0]), user = JSONToDictionary(aStatus["user"]), username = JSONToString(user["name"]) { callback(.Value(Box(username))) return } Amit Bijlani, @paradoxed - 2015
  20. For Example func half(x: Int) -> Int? { // if

    x is even return x/2 if x % 2 == 0 { return x / 2 } return .None } Amit Bijlani, @paradoxed - 2015
  21. Monads in Swift Binds an Optional to a Function that

    takes a non-optional and returns an Optional A? >>> A -> B? -> B? Amit Bijlani, @paradoxed - 2015
  22. Monads in Swift func monad<A, B>(a: A?, f: A ->

    B?) -> B? Amit Bijlani, @paradoxed - 2015
  23. Monads Bind Operator 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 } } Amit Bijlani, @paradoxed - 2015
  24. Modifying Cast Functions To accept non-optional parameters func JSONToString(object: JSON)

    -> String? { return object as? String } func JSONToInt(object: JSON) -> Int? { return object as? Int } func JSONToDictionary(object: JSON) -> JSONDictionary? { return object as? JSONDictionary } func JSONToArray(object: JSON) -> JSONArray? { return object as? JSONArray } Amit Bijlani, @paradoxed - 2015
  25. Using the Bind Operator if let statuses = jsonObject >>>

    JSONToArray, aStatus = statuses[0] >>> JSONToDictionary, user = aStatus["user"] >>> JSONToDictionary, username = user["name"] >>> JSONToString { callback(.Value(Box(username))) return } Amit Bijlani, @paradoxed - 2015
  26. User Model struct User { let name: String let profileDescription:

    String let followersCount: Int ... } Amit Bijlani, @paradoxed - 2015
  27. Creating a User Object if let statuses = jsonObject >>>

    JSONToArray, aStatus = statuses[0] >>> JSONToDictionary, user = aStatus["user"] >>> JSONToDictionary { if let username = user["name"] >>> JSONToString, let profileDescription = user["description"] >>> JSONToString, let followersCount = user["followers_count"] >>> JSONToInt { let u = User( username: username, profileDescription: profileDescription, followersCount: followersCount ) { callback(.Value(Box(u))) return } } } Amit Bijlani, @paradoxed - 2015
  28. Implementing fmap infix operator <^> { associativity left } //

    fmap (usually <$>) //func >>><A, B>(a: A?, f: A -> B?) -> B? // bind operator func <^><A, B>(f: A -> B, a: A?) -> B? { if let x = a { return f(x) } else { return .None } } Amit Bijlani, @paradoxed - 2015
  29. Implementing apply infix operator <*> { associativity left } //

    apply func <*><A, B>(f: (A -> B)?, a: A?) -> B? { if let x = a { if let fx = f { return fx(x) } } return .None } Amit Bijlani, @paradoxed - 2015
  30. Currying "If we give a function fewer parameters than it

    takes, it will return a function that takes the remaining parameters" struct User { let name: String let profileDescription: String let followersCount: Int static func create(name: String)(profileDescription: String)(followersCount: Int) -> User { return User(name: name, profileDescription: profileDescription, followersCount: followersCount) } } Amit Bijlani, @paradoxed - 2015
  31. Behold if let statuses = jsonObject >>> JSONToArray, aStatus =

    statuses[0] >>> JSONToDictionary, userDictionary = aStatus["user"] >>> JSONToDictionary { let user = User.create <^> userDictionary["name"] >>> JSONToString <*> userDictionary["description"] >>> JSONToString <*> userDictionary["followers_count"] >>> JSONToInt { if let u = user { callback(.Value(Box(u))) return } } } Amit Bijlani, @paradoxed - 2015
  32. Avoiding Multiple Returns func parseData(data: NSData?, callback: (Result<User>) -> ())

    { var jsonError: NSError? let jsonObject: AnyObject! = NSJSONSerialization.JSONObjectWithData(...) if let err = jsonError { callback(.Error(err)) return } if let statuses = jsonObject >>> JSONToArray, aStatus = statuses[0] >>> JSONToDictionary, userDictionary = aStatus["user"] >>> JSONToDictionary { let user = User.create <^> userDictionary["name"] >>> JSONToString <*> userDictionary["description"] >>> JSONToString <*> userDictionary["followers_count"] >>> JSONToInt if let u = user { callback(.Value(Box(u))) return } } callback(.Error(NSError())) } Amit Bijlani, @paradoxed - 2015
  33. Breaking up the function • Parse the response • Parse

    the data into JSON • Parse the JSON into our User object Amit Bijlani, @paradoxed - 2015
  34. Argo Example struct User { let name: String let email:

    String? let role: Role } extension User: JSONDecodable { static func create(name: String)(email: String?)(role: Role) -> User { return User(name: name, email: email, role: role) } static func decode(j: JSONValue) -> User? { return User.create <*> j <| "name" <*> j <|? "email" // Use ? for parsing optional values <*> j <| "role" // Custom types that also conform to JSONDecodable just work } } Amit Bijlani, @paradoxed - 2015
  35. Argo Example let json: AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(0), error:

    .None) if let j: AnyObject = json { let value = JSONValue.parse(j) let user = User.decode(value) } Amit Bijlani, @paradoxed - 2015
  36. SwiftyJSON let json = JSON(data: dataFromNetworking) if let userName =

    json[0]["user"]["name"].string{ //Now you got your value print(userName) } Amit Bijlani, @paradoxed - 2015