Rust and Swift (xiv)
Indexing and subscripts, or: traits vs. keywords again.
I am reading through the Swift book, and comparing it to Rust, which I have also been learning over the past few months. As with the other posts in this series, these are off-the-cuff impressions, which may be inaccurate in various ways. I’d be happy to hear feedback! Note, too, that my preferences are just that: preferences. Your tastes may differ from mine. (See all parts in the series.)
Rust and Swift both support defining subscript access to a given data type, like SomeType[accessedByIndex]
. Unsurprisingly, given everything we’ve seen so far, Rust does this with traits, and Swift with a keyword.
In Rust, you can define subscript-style access to a type by implementing the Index
and/or IndexMut
traits, which allow indexing into a given location in a kind of type. The implementation simply requires one function, which is called when you use the []
operator. That function, index
or index_mut
, implements how to do the lookup for the specific type. The impl
block indicates not only that Index
or IndexMut
is being implemented, but also the type of the key used: impl Index<Bar> for Foo { ... }
, where access would look like a_foo[some_bar]
.
The two kinds of traits and corresponding methods define the behavior for immutable and mutable data type, as their name suggest.
Since the trait is defined generically, you can implement whatever kinds of accessors you like to the same underlying data structure, including generics accessors with trait bounds.
It is perhaps telling that in Rust you just find these traits in the general std::ops
module, where all the core language operations and associated operators are defined. Rust doesn’t do “operator overloading” so much as it simply provides operators as one more class of trait potentially applicable to your type. (The family resemblance to Haskell’s type classes and similar in other languages is obvious.)
In Swift, you define indexing behavior with the subscript
keyword. Subscripts act very similarly to Swift’s computed properties. They can be made read- or write-only by including or excluding get
and set
function definitions, just like computed properties.
The behavior is in fact so closely aligned with the computed property syntax and behavior that I initially wondered if it wasn’t just a special case. It is not (though I’m sure much of the parsing machinery can be shared). As the designation of subscript
as a keyword strongly implies, and unlike in Rust, this is a separate language construct, not building on existing language machinery.
Swift, like Rust, allows you to define arbitrary accessors. However, since the behavior relies on the subscript
construct rather than generics and protocols (Swift’s equivalent to Rust’s traits), you define different kinds of accessors via multiple subscript
blocks. (Presumably these could take generic arguments, but I haven’t tested that to be sure.)
Both languages proceed to use these as ways of accessing types as makes sense—e.g. for not only arrays or vectors, but also dictionaries in Swift and HashMap
types in Rust.
Since you can define the behavior yourself, you can also use complex types as keys. The languages approach this a bit differently, though. In Rust, if you wanted a compound key, you would need to define either a simple container struct
or use a tuple as the argument. In Swift, because it uses the same basic syntax as computed properties, you can just define as many method arguments, of whatever type, as you want.
Takeaway: Rust uses traits; Swift uses a keyword. We probably could have guessed that when we started, at this point!