⬆️ ⬇️

Swift 2 in everyday life. Another JSON parser

A couple of months ago, Apple released a major update of its new offspring - Swift 2. It released it, which is called the apple-way, and not the way that "everything is very good and you do not need to think about anything, just use", but another. “We know that this is better, but it was worse before, so drop everything before and start using it” - that is, a language with noticeable problems with backward compatibility - starting from the fact that the regular migration tool works quite unstable, and ending with , by the fact that you definitely cannot develop a new version of the language without updating the entire developer’s toolkit until the last is not very stable last - and, worst of all, in the opposite direction too. But it's not about that. It will be about what I like Swift 2. Unfortunately, since the language is still not recognized by the community as production-ready, the vast majority of materials about it can be attributed to two categories - the documentation of Apple and “I played here in the evening and I got cool. " There are exceptions, of course, but there are not enough of them, so I will try to light up this language a little from the position of working with and on it.

In this article, I would like to talk about the standard ORM task of JSON deserialization - that is, how to get some deserialized structure from the NSDictionary [String: AnyObject] object. What has changed with the advent of Swift 2? How was it before? In addition, we will consider this problem from the point of view of the near-functional approach, which imposes certain limitations, such as the immobility of once created data, for example. Therefore, the solution in question may be somewhat more complicated than the others - but well, okay.

image






So, what are the requirements for a deserializer?



On Swift 1, there are the following approaches to working with JSON objects:



So let's start with what we work with:

To begin with, we will introduce a protocol, the correspondence to which we will designate the possibility of obtaining this object from some JSON object.



 protocol deserializeable {
     func decode () -> Self?
 }


Now I would like to make another small digression:

We want to work with type-safe code and generally do not really save on types

And in real code it would be nice to distinguish an int that matches a person’s age

and Int, corresponding to the temperature behind the aircraft - therefore, in order to avoid ( “obsession with primitives” )

and be able to enter domain restrictions at the type level, such as, for example, age cannot be negative.

Therefore, wrappers for scalar types appear in the code in one form or another.

')

 struct NamedInt {
     let value: int
 }
 struct NamedString {
     let value: String
 }

 struct LeafObject {
     let BunchOfInts: [NamedInt]
     let optionalString: NamedString?
     let someDouble: Double
 }

 struct RootObject {
     let leaves: [LeafObject]
 }


And for this we will deserialize the following dictionary:



 ["Childs":
     [
         [
             "Ints": [1, 2, 3]
             "String": "superString",
             "Double": 1.25
         ],
         [
             "Ints": [],
             "Double": 2.5
         ]
     ]
 ]


The full code discussed later can be found at the Github link: JSON_1.playground. In order for it to compile and work for you - it is worth opening it all the same in XCode 7, but it is written entirely in the ideology of Swift 1.2 - we will work with it.



So, let's look at the main aspects of this code.



Currying



Why: In the above example, the code has one restriction mentioned earlier - all the structures we work with in this article are immaterial, that at the level of the language solves problems for us that someone from the structure users can, potentially, do not work with the data that came from the model due to the fact that they were replaced by someone. Someone this approach may seem redundant. And, in general, it is true - structures, unlike classes, “by value” —that is, when this structure is transferred from the model — it will be copied — which leads to additional memory and processor time costs. Changing one field in the data structure is also obviously faster than creating a new instance, but nonetheless for a long time such a compromise is considered justified - because it provides us with such advantages as



One way or another, this is a conscious choice, so we build our deserialization infrastructure under this assumption.

So, we have an immutable object. What operations does Swift offer us to work with? In addition to the field getters, Swift offers a default constructor to which we pass all the fields of the structure being created.

Each field has a clear and strict type - and this, on the one hand, is very good - because we physically cannot even try to place an unsuitable object in the wrong fields, but on the other hand, if at some stage deserialization did not work (for example, , in the source dictionary there is no field with the necessary key) - we need to interrupt the process.



And here curry comes to the rescue.



 func curry <A, B, C, R> (f: (A, B, C) -> R) -> A -> B -> C -> R {
     return {a in {b in {c in f (a, b, c)}}}
 }


It turns the LeafObject constructor (BunchOfInts: [NamedInt], optionalString: NamedString ?, someDouble: Double) into a chain of functions [NamedInt] -> (NamedString? -> (Double -> LeafObject)) to which we can sequentially apply deserialized fields.



Optional partial application



Actually, the second part of deserialization is the interruption of the process in the event that we did not succeed in a specific stage. Here we will use the <*> operator, defined as follows:



 func <*> <A, B> (f: (A -> B) ?, x: A?) -> B?  {
     if let f1 = f, x1 = x {
         return f1 (x1)
     }
     return nil
 }


What is he doing? It only says that the record f <*> x is in some sense equivalent to the record f (x). Why in a sense? Because unlike f (x) - if x (or f) for one reason or another is equal to nil - then the result of the whole expression will also be nil - while the function f will not be called.

That is, if we have some function f (x, y, z), then we can convert it to currying x -> y -> z -> f and calculate how

curry (f) <*> x <*> y <*> z. What does this give us? For example, if at some stage — for example, when calculating y — we had an error — and y is equal to nil — then the remaining parts of the expression (z) will not be calculated and, moreover, all this together will not cause the function f itself.



Similarly, you can define a <?> Operator for which nil is a valid value.



Apply



This can be attributed to the syntactic sugar, which is necessary for the greater declarativeness of the described, as well as, again, in order to simplify the work with what is called monad binding, a single rule for the work of a group of functions.

The apply >>> = operator makes the f (x) and x >>> = f constructions almost equivalent. What does it have to do with binding and monads? And for this you can read about the Maybe monad, or about its slightly wider interpretation - Result, about which you can read, for example, here

In fact, this operator allows each function to work with simple input parameters, while giving out more information than is necessary for the next function in the chain (error). And if this error occurs, it rises to the top without having to calculate the remaining parts of the expression



Accordingly, looking at the example of filling one specific structure



 return curriedConstructor
     <*> dict >>> = objectForKey ("ints") >>> = asArray >>> = decodeArray
     <?> dict >>> = objectForKey ("string") >>> = NamedString.decode
     <*> dict >>> = objectForKey ("double") >>> = Double.decode




We see the sequential use of calculated arguments and now we know that if some argument could not be calculated, then the whole chain is interrupted.



There are a few unparsed moments in this code, but I think they are quite understandable from the code that is on the githaba, and therefore I will not dwell on them much.

So, so we have an example of deserialization in terms of Swift 1.2

What are its advantages?



So why do you actually want to redo it?



Let's try to consistently get rid of these shortcomings.



So, Swift 2



The final file after rewriting this code on Swift 2 can be found here Github : JSON_2.playground. As you understand, it is absolutely necessary to open it at least on Xcode 7 beta 6 in order to work correctly. (it was in beta 6 that the map function signature was added for togo, so that it could work with throws parameters. Therefore, in order to run this code on beta 5, you will need to write this function yourself)



The first and one of the main changes that swift 2 has brought us is exception-like error handling. (For those who have not yet dived into this topic, you can read, for example, here ). What useful properties does this approach have?

In short, this is an extremely easy-to-use monad. And if in a little more detail, here are some of the consequences of the operations that the language suggests:



Chaining



if we have



 func foo (a: Int) throws -> Int
 func bar (b: Int) throws -> Int


then instead of code



 do {
     let q = try foo (1)
     let b = try bar (q)
 } catch {}


we may well write the following



 do {
     let q = try bar (foo (1))
 } catch {}


Moreover, we can use exactly 1 try within a single operation regardless of its complexity.



Rethrow



Moreover, we don’t always need do ... catch sugar - if the function does not involve error handling, but only passes further upward errors of internal functions and still throws some of its own



 func foo (a: int) throws -> int {
     return try bar (a + 1)
 }


Function flow with errors



throw has about the same meaning as return, so we can easily write functions of this kind:



 func foo (a: int) throws -> int {
     if a> 0 {
         return a
     } else {
         throw someError
     }
 }


That is, functions in which there are execution paths that do not pass through return



Yes, it is a monad that is very easy to use.



Work with it is exactly as in the picture.

image

That is, interlocking calls to functions in a chain, as a consequence of 1 and 2 - we can not care about error handling at each intermediate stage - we can deal only with the result of the latter. Moreover, the transition from the green success track to the red error track is carried out with minimal labor costs - it is enough that somewhere (perhaps deep inside) a throw will work.



So. Using these advantages, we will try to formulate how we would like to perform deserialization in the future:



We have a LeafObject constructor (BunchOfInts: [NamedInt], optionalString: NamedString?, SomeDouble: Double)

So why not fill throws with entities?



 LeafObject (
     BunchOfInts: try getInts,
     optionalString: try getString,
     someDouble: try getDouble
 )


In addition, as stated above - this try can be taken out



 try LeafObject (
     BunchOfInts: getInts,
     optionalString: getString,
     someDouble: getDouble
 )


In this case, in whatever step of the calculation an error occurs, the process of building the object will stop.

Well, let's try to come to this in the code.

To do this, we need two auxiliary classes - DictionaryDecoder and ArrayDecoder - from the names, in general, it is clear what they are doing - a sort of Helper Classes for decoding. So, let's start with something quite simple. No try / catch - just get the value from the dictionary



 struct DictionaryDecoder {
     private let dict: [String: AnyObject]
     init? (_ value: AnyObject) {
         if let dict = value as?  [String: AnyObject] {
             self.dict = dict
         } else {
             return nil
         }
     }
    
     func decode <T: Deserializeable> (forKey key: String) -> T?  {
         return T.decode (self.resultForKey (key))
     }

     private func resultForKey (key: String) -> AnyObject?  {
         return self.dict [key]
     }
 }


And now let's see what this does not suit us:

  1. decode returned an object of type T, or nil, if it didn't work out. What if we want to get T at the output?
  2. the object could not zadekodit - why?
  3. DictionaryDecoder we have an invalid dictionary entry. For example, we expect that some node of the dictionary being decoded will turn out to be a dictionary - and it turned out to be an array. Or scalar value.


This looks like obvious candidates for throw - but for this we need to change our Deserializeable protocol a little bit:



 protocol deserializeable {
     func decode () throws -> Self
 }


And make the appropriate changes to the DictionaryDecoder



 struct DictionaryDecoder {
     private let dict: [String: AnyObject]
     init (_ value: AnyObject) throws {
         guard let dict = value as?  [String: AnyObject] else {throw NotADictError (/ * some details * /)}
         self.dict = dict
     }

     func decode <T: Deserializeable> (forKey key: String) throws -> T {
         return try T.decode (self.resultForKey (key))
     }

     func decode <T: Deserializeable> (forKey key: String) throws -> T?  {
         return try self.optionalForKey (key) .map (T.decodeJSON)
     }

     private func resultForKey (key: String) throws -> AnyObject {
         guard let value = self.dict [key] else {throw KeyMissingError (/ * some details * /)}
         return value
     }

     private func optionalForKey (key: String) -> AnyObject?  {
         return self.dict [key]
     }
 }


And besides, in the deserialization of atomic entities including



 protocol ScalarDeserializeable: Deserializeable {}
 extension ScalarDeserializeable {
     static func decode (input: AnyObject) throws -> Self {
         guard value value input as?  Self else {throw UnexpectedTypeError (/ <em> some details </ em> /)}
         return value
     }
 }

 extension Int: ScalarDeserializeable {}
 extension String: ScalarDeserializeable {}
 extension Double: ScalarDeserializeable {}


What we have written above is enough to deserialize a simple hierarchical dictionary, in which each specific value is scalar.

Now let's turn our attention to some important features of what we have just written:

  1. In the last part - ScalarDesrializeable, we hooked on one more important innovation of Swift 2 - mixins, they are also Protocol extensions, which allow us to provide default implementation of protocol methods. That is, having written such a code, we automatically added the decode method and correspondence to the Deserializeable protocol with a scalar class. You can read more about this anywhere, where they talk about Swift 2 :-). But, for example, here

  2. Before that, in the decoder, we could add a distinction between optional and non-optional types in the decoder — now if we want to parse the optional parameter, we find that nothing is found using this key and that what lies according to this key could not be deserialized. (using optionalForKey instead of resultForKey generates one less error)

  3. As described above - we used try chaining

    In the return try T.decode (self.resultForKey (key)) construct, both decode and resultForKey can fail with an error, but if the internal fails with an error, the decoding will not even begin.



So. Let us deal with the last stage and deal with errors: UnexpectedTypeError, KeyMissingError, NotADictError in the previous code is still very uninformative.

To do this, we will create two types to describe the error:



  1.   enum SchemeMismatchError {
         case NotADict
         case KeyMissing
         case UnexpectedType (expectedTypeName: String)
     }
    


Which keeps in itself what particular error happened

  1.   struct DecodingError: ErrorType {
         let error: SchemeMismatchError
         let reason: String
         let path: [String]
     }
    


ErrorType - another innovation Swift 2 - protocol, inheriting from which - you get the opportunity to use objects of this type in throw (and only them)

In this structure, we see, in fact, an error that happened to us and an array of String, in which we are going to store the path to a specific error, which makes it easier to localize (for which everything was actually started). As we can see, it is also let - since here we are not going to deviate from the course taken for data immunity.



In general, it’s pretty obvious that the above code will replace all throws - with constructions like throw DecodingError (error: .NotADict, reason: "expected dictionary", path: []), or some abbreviated forms

But now we need to consistently fill the path. In order to understand how we can get it - let's write out what steps we are separated from the beginning of the dictionary deserialization to the specific value deserialization - something like



 DictionaryDecoder.init -> resultForKey -> decode -> ... -> resultForKey -> decode


That is, in fact, only three different steps that are applied recursively in one call. Accordingly, no matter what level of error occurs - it will rise along the reduced chain in the opposite direction - and we just need to add one value to the path during the stages of this chain. For everyone? No, it is enough, only at the decode level - because for each level of the dictionary hierarchy to our specific meaning, there is one and exactly one decode (to lower it down the level). How do we do this? For this, we actually write catch and our decode will look like this:



 extension DecodingError {
     func errorByAppendingPathComponent (component: String) -> DecodingError {
         return DecodingError (error: error, reason: path, path: [component] + path)
     }
 }

 func decode <T: Deserializeable> (forKey key: String) throws -> T {
     do {
         return try T.decode (self.resultForKey (key))
     } catch let error as DecodingError {
         throw error.errorByAppendingPathComponent (key)
     }
 }


Please note that in catch we do not handle all errors, but only DecodingError. Why?

  1. Because we know that our code does not send any other errors.
  2. Because even if the code sends other errors - they will not be lost at this stage - but simply pass on.

    In fact, at the moment it is a little sad that there is no possibility to strictly typify the errors that are thrown out, but so far we live with it.


Now we have enough functionality and deserialization of our entity as follows:



 extension LeafObject: Deserializeable {
     static func decode (input: AnyObject) throws -> LeafObject {
         let dict = try JSONDictionary (input)
         return try LeafObject (
             BunchOfInts: dict.decode (forKey: "ints"),
             optionalString: dict.decode (forKey: "string"),
             someDouble: dict.decode (forKey: "double"),
         )
     }
 }


I do not know about you, but I like _ . This is probably all.

The code given in the playground is slightly different from the one shown here in the direction of expansion, so that it can be played with deserialization of more complex entities. As homework - try, for example, to deserialize enum, consisting of some complicated structures - for example:



 enum graph {
     case Tree (RootObject)
     case Forest ([RootObject])
     case SimpleNode (LeafObject)
 }


So, as a conclusion, we came to:

  1. try / catch is convenient - it was really lacking in Swift 1.2 - so try not to write on it anymore :-)
  2. This code, obviously, does not meet the most stringent speed requirements - but it can be used quite well in a typical client-server application. Last but not least, due to the fact that it is easy to work with errors during development - when the server changes frequently and the client falls off.
  3. I like the direction in which Swift is moving - because when switching to a new version of the language from the code, the unobvious constructs from the first version of parsing are gone, trivially, unnecessarily


PS If you read this far, then, my friends, iOS-developers - do not be afraid - go to Swift. It really deserves attention, as the main language of the application (even if homework). I'm still waiting and I can’t wait for the real need to have a part of the project on Objective-C (not third-party dependencies, a huge number of which is simply stupid to refuse, namely the significant part of its internal code base, which must be maintained day-by-day) . It is concise, compact and elegant. Well, yes enough for now.

Source: https://habr.com/ru/post/266657/



All Articles