Edit: there is a follow-up to this post, giving the case against this idea, which you should read after this one.
My not-statistically-proven assertion (after working in the LOB development mines for years) is that Null Pointer Exception is the #1 cause of crashes for apps written in memory-managed languages.1
The hope is, by introducing optionals, that this kind of error get pushed way down the leaderboard in Swift. Sure you can still force-unwrap a nil value. But you have to load the gun, cock it, and then point it at your foot. I love that the force-unwrap operator is an exclamation mark.
Who is the aspiring hopeful cause of crashes, eyeing that vacated top spot? I’m guessing the Array Out of Bounds exception.
This one is still very much at large in Swift. Int
is the index for arrays, and there’s nothing stopping you blatting straight past the end, getting a runtime assertion or possibly even scribbling into memory depending on what you’re doing and how you’re compiling.
This is really just a step up from pointers.2 There’s just too much leeway for unreformed C programmers to write the same crappy old code:
// But it worked when I tested it! // (with an odd-sized array) func reverseInPlace<T>(inout a: Array<T>) { var start = 0 var end = a.count - 1 while start != end { swap(&a[start], &a[end]) start++; end-- } }
It doesn't have to be this way. It's totally fixable, just like null pointers were. There are two good options, an easy one and a (slightly) harder one.
The easy one
Make Array.subscript
return an optional. Only if your index is within bounds will it return a value, otherwise it'll return nil
. This is like the approach Dictionary
takes with lookups. If your dictionary doesn't contain a particular key, you get a nil
.
“But that's because dictionaries are different” you say. No they aren't. Dictionary
has a method to check if a key is present. You could call that first and then, if it is, get the value. But people don't want to do that, so they skip straight to the getting out the value part, and because that's a bit risky, it returns nil
if the key isn't there. Likewise, Array
has methods for checking if the array is empty or if an index is beyond the end. You don't have to bother checking for that, but if you mess up, boom!
The most common case is probably getting the first element with a[0]
. So common is this that beta 5 introduced Array.first
. Which returns… an optional. If that doesn't convince you, I don't know what will.
I expect this change would elicit some moaning. Similar to the complaints about how you can't index randomly into the middle of a Swift string. But as with the string case, the question really is, how often do you need to do this anyway? Use for...in
, use find
, use first
and last
. Don't hack at your array like a weed.
Developers who hate it and want the old behaviour could just stick a !
after their subscript access and it'd be back to the way it was. Presumably these people are compiling -Ounchecked
. Let's see whether their users find the snappy performance makes up for the random crashing.
The harder one
Stop using Int
for indexing into random-access collections. Use a real index object.
Again, Dictionary
already does this. To index over/into a dictionary you have to get a DictionaryIndex
object, which has no public initializers so can only be fetched via methods on Dictionary
.
If an index type is random-access (which Dictionary
’s isn't), then they can be compared, added together, advanced by numeric amounts in O(n), just like an integer can.
To allow arrays to return a non-optional value when subscripted with this index, you'd need to tweak the index object to have successor()
etc return an optional, with nil
for if it's gone beyond the collection's bounds. This way, the index can be guaranteed to point to a valid entry.3
Again, this would make them more of a pain to use, in exchange for safety. But the other downside to this approach is to implement this, indices would need to know about the specific collection they index. Which means they'd need a reference to it, which introduces other complications like reference cycles and an increased size. Not a deal-breaker though.
Indices knowing about their container would have the side benefit of allowing them to become dereferenceable (see a previous post for more on this).
It would also allow them to guard against the following, which compiles and runs but is presumably a bad idea:4
let d1 = [1:1, 2:2, 3:3] let d2 = [7:7, 2:2, 1:1, 4:4] // get an index from d1 let idx = d1.startIndex.successor() // and use it on d2... d2[idx] // returns (2,2)
No need to pick one
These two approaches are not mutually exclusive. Array
could provide both an Int
subscript and an index one – the former giving back an optional, the latter not.
I like that idea. The index option has downsides though. There's also a compromise, where the Int
version is checked, but the index object version is unchecked, on the basis that if you're using indices you're thinking a bit more carefully about what you're doing.
But here's hoping at least the first option makes it into a subsequent beta before the current implementation's set in stone.
If there's a reason I'm missing that means these schemes are hopelessly naive, let me know at @airspeedswift on twitter.
- In non-managed code, the number one cause of crashes is crap, I still haven’t found the problem, where did my whole day go? ↩
-
Obviously that’s a long step. You can’t do in Swift my favourite silly thing to do in C:
char s[] = "hello"; int i = 3; cout
<<i[s]
<<endl;
↩ - All this is assuming the indices are into collections that aren't changing underneath them. Indexing mutating collections is a whole different ball-game. ↩
-
Maybe it's ok? If the index is to a key common to both, it works. If it's an index to a key not in
d2
, you get an invalid index assertion. Still probably a bad idea. ↩
[…] got a lot of interesting feedback from my previous article, regarding the proposal to change array subscripts to return optionals. Some pro, some […]