Rust and Swift (xii)
Properties: type and instance, stored and computed.
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.
Box
orRc
orArc
, doesn’t change this fundamental reality.) This underlying difference gives rise to one the big difference between Swift classes and Rust structs: a constantclass
instance in Swift can still have its fields mutated; not so with a Ruststruct
instance. But also not so with a Swiftstruct
instance, as it turns out! There isn’t a straightforward way to do this withBox<T>
in Rust; you could do it with something like anRc<T>
orArc<T>
, though.Swift’s
lazy
keyword, 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 havinglazy
properties, 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?)Swift has computed properties, a concept familiar to Python developers (and relatively recently introduced in JavaScript). These can be quite handy, as they let you define a property to be accessed like any other (
someInstance.theProperty
) while being defined with functions which compute the value dynamically. A common, trivial example: if you defined aPerson
withfirstName
andlastName
members, 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
struct
method 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
impl
fromstruct
andenum
, 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
protocol
type classes, which are analogous to Rust’strait
s—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
newValue
argument 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:
willSet
anddidSet
. 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 theset
method 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
Observable
trait, 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?
Both of these questions can be answered by reading the source code for the languages (Rust, Swift), of course. Putting that on my to-do list.
Swift also has type properties: values common to all instances of a given type. These are directly analogous to class properties (or class attributes) in Python or prototype properties in JavaScript.
Rust doesn’t have anything like this to my knowledge. You could accomplish something similar using a module-level variable with a
'static
lifetime,3 much as you could in C—but that wouldn’t be an item on the type itself, of course.The
static
declaration of item in Swift suggests what a possible implementation might look like in Rust: defining a member likea_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.
leaving aside details about
trait
specialization still being hashed out in Rust↩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
static
keyword in Swift, like that in C, Objective-C, and C++, is sort of like Rust’s'static
lifetime specifically, for variables at least—but Rust’s lifetime is substantially more sophisticated and complex than that analogy might suggest.↩