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.map
twice for an array. If thispure
transformations, then you can combine the two map
into one map
and 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