
JSON parsing and which collected the most questions on this talk.JSON data from Brent and David’s skeptical colleagues: var json : [String: AnyObject] = [ "stat": "ok", "blogs": [ "blog": [ [ "id" : 73, "name" : "Bloxus test", "needspassword" : true, "url" : "http://remote.bloxus.com/" ], [ "id" : 74, "name" : "Manila Test", "needspassword" : false, "url" : "http://flickrtest1.userland.com/" ] ] ] ] JSON data in a typed and secure way into an array of Swift structures: struct Blog { let id: Int let name: String let needsPassword : Bool let url: NSURL } JSON structure becomes very simple.JSON structure is incorrect (for example, the name missing or the id is not an integer), then the result is nil .reflection nor KVO , we just have a couple of simple functions and some smart way to combine them: func parseBlog(blog: AnyObject) -> Blog? { return asDict(blog) >>= { mkBlog <*> int($0,"id") <*> string($0,"name") <*> bool($0,"needspassword") <*> (string($0, "url") >>= toURL) } } let parsed : [Blog]? = dictionary(json, "blogs") >>= { array($0, "blog") >>= { join($0.map(parseBlog)) } } dictionary . This function takes as input the input: [String: AnyObject] and a specific key: Sting , and tries to find in the original dictionary input by the key key also a dictionary, which will be the return value of the Optional : func dictionary(input: [String: AnyObject], key: String) -> [String: AnyObject]? { return input[key] >>= { $0 as? [String:AnyObject] } } JSON data, we expect that the key "blogs" is a dictionary. If the dictionary exists, then we return it, otherwise we return nil . We can write similar functions for arrays ( array ), strings ( strings ) and integers ( int ) (here the signature is only for these types, and the full code is on GitHub ): func array(input: [String:AnyObject], key: String) -> [AnyObject]? func string(input: [String:AnyObject], key: String) -> String? func int(input: [NSObject:AnyObject], key: String) -> Int? JSON data. This is a dictionary that is present in the structure under the key "blogs" . And under the key "blog" contains an array. For such parsing we can write the following code: if let blogsDict = dictionary(parsedJSON, "blogs") { if let blogsArray = array(blogsDict, "blog") { // - } } Optional value and applies the function f to it only if this Optional not nil . This forces us to use the flatten “leveling” function, which removes (“aligns”) the nested Optional and leaves the only Optional . infix operator >>= {} func >>= <A,B> (optional : A?, f : A -> B?) -> B? { return flatten(optional.map(f)) } func flatten<A>(x: A??) -> A? { if let y = x { return y } return nil } <*> operator. To parse a single blog (Blog structure), we will have the following code: mkBlog <*> int(dict,"id") <*> string(dict,"name") <*> bool(dict,"needspassword") <*> (string(dict, "url") >>= toURL )Optional values ​​are non- nil : mkBlog(int(dict,"id"), string(dict,"name"), bool(dict,"needspassword"), (string(dict, "url") >>= toURL)) <*> operator. It combines two Optional values: it takes a function as the left operand, and a parameter of this function as the right operand. He checks that both operands are not nil , and only then he applies the function. infix operator <*> { associativity left precedence 150 } func <*><A, B>(l: (A -> B)?, r: A?) -> B? { if let l1 = l { if let r1 = r { return l1(r1) } } return nil } mkBlog ? This is the curried function (curried function), which “wraps” our initializer.(Int, String, Bool, NSURL) -> Blog .curry function converts it to a function of type Int -> String -> Bool -> NSURL -> Blog : let mkBlog = curry {id, name, needsPassword, url in Blog(id: id, name: name, needsPassword: needsPassword, url: url) } mkBlog together with the <*> operator. // mkBlog : Int -> String -> Bool -> NSURL -> Blog // int(dict,"id") : Int? let step1 = mkBlog <*> int(dict,"id") mkBlog and int (dict,"id") using the <*> operator gives us a new function of the type (String -> Bool -> NSURL -> Blog)? . And if we combine it with a string: let step2 = step1 <*> string(dict,"name") (Bool -> NSURL -> Blog)? . And if we continue to do this, then we end up with the Optional value of Blog? .JSON parsing really very simple. Instead of Optional , you could also use another type, which includes errors (errors), but this is a topic for another post.JSON parsing, and answers to questions.JSON dictionaries into regular, typed dictionaries.JSON data are considered and the same task is set to convert them into an array of blogs [Blog] , as in the above article.parseBlog function returns Blog if the types of all components are correct.if continue to run for each key and the corresponding type only if the conditions are met.Blog value with the correct types and values ​​from the Optionals values. func parseBlog(blogDict: [String:AnyObject]) -> Blog? { if let id = blogDict["id"] as NSNumber? { if let name = blogDict["name"] as NSString? { if let needsPassword = blogDict["needspassword"] as NSNumber? { if let url = blogDict["url"] as NSString? { return Blog(id: id.integerValue, name: name, needsPassword: needsPassword.boolValue, url: NSURL(string: url) ) } } } } return nil } string function that checks whether the type is NSString , because in our case we used it twice. This function takes a dictionary, searches for the key and returns the corresponding value only if the key matches the string. Otherwise, it returns nil . func string(input: [String:AnyObject], key: String) -> String? { let result = input[key] return result as String? } string function, and looks like this: func parseBlog(blogDict: [String:AnyObject]) -> Blog? { if let id = blogDict["id"] as NSNumber? { if let name = string(blogDict, "name") { if let needsPassword = blogDict["needspassword"] as NSNumber? { if let url = string(blogDict, "url") { return Blog(id: id.integerValue, name: name, needsPassword: needsPassword.boolValue, url: NSURL(string: url) ) } } } } return nil } numbers we will create a function similar to the string function. This function searches for a number and makes a cast, if it exists. We can create similar functions for the int and bool types. For Optionals we can also use map , which is executed only if the value exists. func number(input: [NSObject:AnyObject], key: String) -> NSNumber? { let result = input[key] return result as NSNumber? } func int(input: [NSObject:AnyObject], key: String) -> Int? { return number(input,key).map { $0.integerValue } } func bool(input: [NSObject:AnyObject], key: String) -> Bool? { return number(input,key).map { $0.boolValue } } func parseBlog(blogDict: [String:AnyObject]) -> Blog? { if let id = int(blogDict, "id") { if let name = string(blogDict, "name") { if let needsPassword = bool(blogDict, "needspassword") { if let url = string(blogDict, "url") { return Blog(id: id, name: name, needsPassword: needsPassword, url: NSURL(string: url) ) } } } } return nil } if . Our flatten function checks whether all Optionals have values, and if so, this “large” Optional as a tuple. func flatten<A,B,C,D>(oa: A?,ob: B?,oc: C?,od: D?) -> (A,B,C,D)? { if let a = oa { if let b = ob { if let c = oc { if let d = od { return (a,b,c,d) } } } } return nil } flatten function flatten and if they are all non- nil , return the Blog . func parseBlog(blogData: [String:AnyObject]) -> Blog? { let id = int(blogData,"id") let name = string(blogData,"name") let needsPassword = bool(blogData,"needspassword") let url = string(blogData,"url").map { NSURL(string:$0) } if let (id, name, needsPassword, url) = flatten(id, name, needsPassword, url) { return Blog(id: id, name: name, needsPassword: needsPassword, url: url) } return nil } if . This will require the functions A,B,C,D -> R , which converts the arguments A,B,C,D to R , as well as the tuple (A, B, C, D) , and if they are not both nil , then the function is applied to the tuple. func apply<A, B, C, D, R>(l: ((A,B,C,D) -> R)?, r: (A,B,C,D)?) -> R? { if let l1 = l { if let r1 = r { return l1(r1) } } return nil } func parseBlog(blogData: [String:AnyObject]) -> Blog? { let id = int(blogData,"id") let name = string(blogData,"name") let needsPassword = bool(blogData,"needspassword") let url = string(blogData,"url").map { NSURL(string:$0) } let makeBlog = { Blog(id: $0, name: $1, needsPassword: $2, url: $3) } return apply(makeBlog, flatten(id, name, needsPassword, url)) } apply with an argument that is our “aligned” structure. But to continue refactoring the code, you need to make the apply function more generalized, and also to perform currying. func apply<A, R>(l: (A -> R)?, r: A?) -> R? { if let l1 = l { if let r1 = r { return l1(r1) } } return nil } apply function again, we can make the code more compact and finally get our Blog . func curry<A,B,C,D,R>(f: (A,B,C,D) -> R) -> A -> B -> C -> D -> R { return { a in { b in { c in { d in f(a,b,c,d) } } } } } // : (Int, String, Bool, NSURL) -> Blog let blog = { Blog(id: $0, name: $1, needsPassword: $2, url: $3) } // : Int -> (String -> (Bool -> (NSURL -> Blog))) let makeBlog = curry(blog) // : Int -> String -> Bool -> NSURL -> Blog let makeBlog = curry(blog) // : Int? let id = int(blogData, "id") // : (String -> Bool -> NSURL -> Blog)? let step1 = apply(makeBlog,id) // : String? let name = string(blogData,"name") // : (Bool -> NSURL -> Blog)? let step2 = apply(step1,name) apply function and after currying, our code has a lot of calls to the apply function. func parse(blogData: [String:AnyObject]) -> Blog? { let id = int(blogData,"id") let name = string(blogData,"name") let needsPassword = bool(blogData,"needspassword") let url = string(blogData,"url").map { NSURL(string:$0) } let makeBlog = curry { Blog(id: $0, name: $1, needsPassword: $2, url: $3) } return apply(apply(apply(apply(makeBlog, id), name), needsPassword), url) } <*> . This is the same as the apply function. infix operator <*> { associativity left precedence 150 } func <*><A, B>(l: (A -> B)?, r: A?) -> B? { if let l1 = l { if let r1 = r { return l1(r1) } } return nil } apply function with our <*> operator. // return apply(apply(apply(apply(makeBlog, id), name), needsPassword), url) // return makeBlog <*> id <*> name <*> needsPassword <*> url } if lets removed. All types are correct, but if we randomly specify other types, the compiler will “complain”. The final version of our code looks like this: func parse(blogData: [String:AnyObject]) -> Blog? { let makeBlog = curry { Blog(id: $0, name: $1, needsPassword: $2, url: $3) } return makeBlog <*> int(blogData,"id") <*> string(blogData,"name") <*> bool(blogData,"needspassword") <*> string(blogData,"url").map { NSURL(string:$0) } } curry by default, but you can write these curry functions with a certain number of arguments, and the compiler will choose one of them for you. Perhaps in the future it will be added to Swift .map and reduce functions?map everything, even arrays and Optionals , and, of course, you have to master it.maptwice for an array. If thispuretransformations, then you can combine the two mapinto one mapand then iterate over the array once. This will be very hard to write optimally in programming languages ​​like C. Clarity is a huge gain, and, besides, I have never experienced performance problems. static func makeBlog(id: Int)(name: String)(needsPassword: Int)(url:String) -> Blog { return Blog(id: id, name: name, needsPassword: Bool(needsPassword), url: toURL(url)) } Source: https://habr.com/ru/post/246965/
All Articles