Swift 2.0: More crashing and throwing in Aldwych (a JSON Parser)


A conversation started on twitter yesterday surrounding the inability of subscripts to throw errors. It was begun by @krzyzanowskim and gave me the opportunity to think even further about the approach to error handling I am employing in Aldwych 2.0 (a JSON parser for Swift).

Standard approach

My standard approach is to follow the Swift standard library way of doing things as much as possible. The core example being to throw errors when something user-led might not be possible: e.g. a file chosen is of the wrong type or the data in the file is corrupted, or the data simply doesn't parse.

This logic means that NSJSONSerialization has throwing methods as standard for creating objects from JSON and for creating JSON from objects. While NSData goes halfway and has a choice of failable initializers and throwing intializers in iOS 9 (Xcode 7 beta 4). For example:
init(contentsOfURL url: NSURL, options readOptionsMask: NSDataReadingOptions) throws
and
init?(contentsOfURL url: NSURL)
And NSURL has neither: you ask for a URL, you get a URL. It just might not be the one you want if the one you want doesn't exist or it's been mistyped. (Although it can be the case that attempts to retrieve a URL, for example using NSBundle, are optionals.)

Whether the duality of NSData init methods is a transitionary position or whether it will be an ongoing choice reflective of NSData's varying position in a chain of operations, I don't know but for now it allows a welcome choice of approaches.

Beyond the bounds: Arrays

Once you've retrieved an array (assuming it is an array and not a dictionary) of AnyObject values from the JSON then attempts to access individual elements must not go beyond the bounds of the array. Arrays don't throw, they don't fail silently, offer optionals, or some valid alternative to your request. You go beyond the bounds and ... crash.

It is at this point, I realised that in one important area I wasn't following Swift at all in the Aldwych library and that was in the area of array subscripting. I had been returning an optional where the Swift standard library has a fatal error crash if an attempt is made to access an index beyond the end of an array.

This was the way things worked in Aldwych 1.0 and I had unthinkingly carried this forward to to the second version of Aldwych. This seemed to fit with the overall sense of freedom and looseness of JSON when I first started writing Aldwych, but actually who does it benefit to allow a hit and miss approach to changes in data? Changes to data should be as predictable as possible.

Lack of subscript throws

Marcin was querying the lack of throw, which I have to be truthful, I hadn't even been aware of because I hadn't tried throwing from a subscript. Deciding instead to veer away from all throwing when updating values in collections (arrays or dictionaries) whether by subscript or other means. And while there were convincing arguments back and forth (including from @Al_Skipp@radexp and @oisdk), my own conclusion for the purposes of Aldwych was to switch from returning optionals when subscripting arrays to return JSONValues directly, and crashing with a fatal error when attempts are made to access elements beyond the bounds of the array.

Consistency

One of the core reasons for this new choice of array behaviour is consistency across types. If programmers are used to a certain behaviour within the rest of Swift (and arguably in most other programming languages too) then it makes sense to continue that behaviour so that their expectations are not turned upside down.

This means that rather than test for optionals or expecting errors to be thrown code should check in advance that what is about to be attempted can be done. This results in code look like this:
if advance(arr.startIndex,1,arr.endIndex) != arr.endIndex && arr[advance(arr.startIndex,1)].canReplaceWithString() == true {
    arr[advance(arr.startIndex,1),.Typesafe] = "Four"
}
If the index is within range and the current type can be replaced with the type we're intending to replace it with, there should be no reason whatsoever for things to go wrong. If they do then there really is an error and it needs to be fixed.

The role of protocols

Consistency goes hand-in-hand with protocols, which are there to make sure wherever possible we adhere to protocols. This is because the more we adopt protocols, the more we can use generics. And generics can do all sorts of magic. But sometimes piecing together all the parts for a particular protocol can be tricky. Even so, if you don't adopt a protocol immediately that you think you should, your mind should always be on following it as closely as possible until all the pieces are in place.

For example: in Aldwych a JSONArray currently adopts SequenceType in order to provide essential for-in functionality, however with Indexable it currently follows the protocol closely but does not adopt it because of some final pieces that need to be worked out. JSONArray type should also eventually adopt CollectionType and so on as well, essentially mimicking the majority of the behaviour of Array.

The potential future

Remaining consistent with the Swift standard library means that if suddenly the common behaviour for accessing an array index becomes throwing behaviour (or returning optionals) then Aldwych should follow, but for now a fatal error is not only consistent with the Swift library but is also a concise and reliable way of avoiding problems.


Endorse on Coderwall

Comments