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.)
A note on publication: I had this drafted in early January and simply forgot to publish it. Whoops!
As noted in my discussion of the product types in Rust and Swift, Swift distinguishes between classes and structs, with the former being reference types and the latter being value types. All structs are value types in Rust. (That you can wrap them in a pointer for heap-allocation with one of the smart pointer types, e.g.
Arc, doesn’t change this fundamental reality.) This underlying difference gives rise to one the big difference between Swift classes and Rust structs: a constant
classinstance in Swift can still have its fields mutated; not so with a Rust
structinstance. But also not so with a Swift
structinstance, as it turns out! There isn’t a straightforward way to do this with
Box<T>in Rust; you could do it with something like an
lazykeyword, and associated delayed initialization of properties has, as far as I know, no equivalent whatsoever in Rust. And while I can see the utility in principle, I’m hard-pressed to think of any time in my working experience where the behavior would actually be useful. Rather than having
lazyproperties, I would be far more inclined to separate the behavior which should be initialized at a later time into its own data structure, and supplying it via inversion of control if it is necessary for an actions taken by other data structures. (This seems—at first blush at least—to be a way of supporting the un- or partially-initialized data types possible in Objective C?)
someInstance.theProperty) while being defined with functions which compute the value dynamically. A common, trivial example: if you defined a
lastNamemembers, you could define a computed property,
fullName, which was built using the existing values.
Rust doesn’t have computed properties at all. This is because of its design decision to deeply separate data from behavior, essentially stealing a page from more pure-functional languages (Haskell etc.). This is (one reason) why you don’t define the implementation of a
structmethod in the same block as the members of the struct. See an excellent explanation here.
It’s also closely related the way Rust favors composition over inheritance (by making the latter impossible, at least for now!). By separating
enum, Rust makes it not only straightforward but normal to define new behavior for a given item separately from the data description. This, combined with the use of traits (like Swift’s protocols) as the primary way of sharing behavior between objects, means that you don’t have to worry about conforming to some interface when you define a given type; it can always1 be defined later, even by entirely other modules or even other crates (packages).
In any case, the result is that it’s not at all Rustic2 to have something like getters or setters or computed properties. It makes sense to have them in Swift, though, which has a more traditionally object-oriented type system (though with some neat additions in the form of its
protocoltype classes, which are analogous to Rust’s
traits—but we’ll come to those in a future post). This is a wash: it’s just a function of the slightly different approaches taken in object design in the two systems. If you have a Swift-style type system, you should have computed properties. If you have a Rust-like type system, you shouldn’t.
I’m shocked—utterly shocked!—to find that Swift provides a default
newValueargument for setters for computed properties, and shorthand for defining read-only properties. By which I mean: I find this kind of thing entirely unsurprising at this point in Swift, but I don’t like it any better. Making so much implicit just rubs me the wrong way. Once you know the language, it’s fine of course: you’ll recognize all the patterns. It just seems, in an interesting way, to add cognitive load rather than reducing it. That may just be me, though!
Interestingly, Swift also allows you to set watchers on given properties—functions called with the new or the removed value whenever the value of the computed property is updated or touched for any reason. It has two of these built in:
didSet. You can override these to get custom behavior when a normal property is about to change. (You can of course just implement the desired behavior yourself in the
setmethod for a computed property.)
Since Rust doesn’t have properties, it doesn’t have anything analogous. I can’t think of a particularly straightforward way to implement it, either, though you might be able do some chicanery with a trait. Of course you can always define a setter method which takes a value and optional callbacks for actions to take before and after setting the value; the thing that’s nice in Swift is that it gives you these as built-in capabilities within the language itself. (Now I’m wondering if or how you could implement an
Observabletrait, though! Might have to play with that idea more later.) It’s worth remembering , in any case, that Rust doesn’t have these because it doesn’t have properties.
Curiously, Swift provides the same functionality for “global” and “local” variables in a given context. In both cases, this is suggestive of the underlying object model for both modules and functions in Swift.
Now I’m curious what the representation of a module is in Swift; is it part of the general object system in some way?
This likewise gets me asking: what is a module in Rust? It’s a block item, clearly, and accordingly defines a scope (as do functions, if and match expressions, and so on). It’s not a compilation unit (as it is in C or C++). What other machinery is attached to it?
Rust doesn’t have anything like this to my knowledge. You could accomplish something similar using a module-level variable with a
'staticlifetime,3 much as you could in C—but that wouldn’t be an item on the type itself, of course.
staticdeclaration of item in Swift suggests what a possible implementation might look like in Rust: defining a member like
a_static_long: 'static i64. There might be some interesting challenges around that, though; I don’t know enough to comment meaningfully. At the least, it seems like it would be an odd fit with the rest of the memory management approach Rust takes, and it would make it a bit harder to reason correctly about the behavior of data in a given type. (There are certainly issues there around mutability guarantees and lifetime checking!)
Because of the differences in underlying approach to data types and implementation, this is one of the areas where the superficially (and sometimes actually) similar languages diverge a lot.
- Previous: Hopes for the next generation of systems programming.
- Next: Methods, instance and otherwise.
This is now my preferred term for “idiomatic Rust”—directly analogous to “Pythonic,” but with the upside of being an actual word, and one with pleasantly evocative connotations to boot.↩︎
There’s nothing analogous to Rust’s concept of explicit lifetimes in Swift, as far as I can tell. The
statickeyword in Swift, like that in C, Objective-C, and C++, is sort of like Rust’s
'staticlifetime specifically, for variables at least—but Rust’s lifetime is substantially more sophisticated and complex than that analogy might suggest.↩︎