Rust and Swift (x)
Classes and structs (product types), and reference and value types.
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.)
Swift and Rust both have “product types” as well as the
enum
“sum types.” In Rust, these arestruct
types; Swift splits them intoclass
es andstruct
s.“Product types” will be much more familiar to programmers coming from a C-like background, or indeed most object-oriented programming languages: these are the same basic kind of thing as classes, structs, and objects in other languages. These include all the value types which compose them, unlike sum types—
enum
—which have only one of the value types which compose them.Right off the bat, I note the Swift book’s somewhat amusing reticence to call out C and C-descended languages:
Unlike other programming languages, Swift does not require you to create separate interface and implementation files for custom classes and structures.
Because there’s such a long list of languages not directly descended from C which do that, right? 😉
Rust differs not only from Swift but from every other modern language I have used in not having a constructor syntax for its instantiations. Whereas C++ has
new NameOfType()
and Python and Swift both haveNameOfType()
, “constructors” for Ruststruct
s are just functions which return an instance constructed using literal syntax, by conventionNameOfType::new()
.Let’s make a
struct
defining a location in a plane, you might do this in Swift (leaving aside initializer values; I’ll come back to those later). These definitions look very similar. Swift:struct Point { var x: Double var y: Double }
Rust:
struct Point { x: f64, y: f64, }
Creating the types looks a little different, though. Here’s a constructor in Swift:
let point = Point(x: 0, y: 0)
And the two ways we could construct the type in Rust, a literal constructor (fairly similar to constructing
dict
literals in Python or object literals in JavaScript):let point = Point { x: 0.0, y: 0.0 };
Or a constructor method,
new
:// "Constructor" impl Point { fn new(x: f64, y: f64) -> Point { Point { x: x, y: y } } } let another_point = Point::new(0, 0);
Observe: these two things in Rust are the same under the covers (though if
Point
s had non-public internals, they would be non-trivially different: you couldn’t construct it with its private members externally). As usual, Rust opts to keep the language relatively small in these core areas. Given the plethora of ways you can construct something in e.g. C++, I count that a big win.Another difference: Swift has syntax for default values; Rust uses a
trait
instead. In Swift, you simply supply the default value in the definition of thestruct
orclass
:struct Point { var x = 0.0 var y = 0.0 } let point = Point()
In Rust, you use
std::default::Default
, which provides a standard value for a given type, and for simple types can be supplied by the compiler even for custom types. Here is the equivalent Rust code:use std::default::Default; #[derive(Default)] struct Point { x: f64, y: f64, } let point = Point::default();
This is reasonable enough, but we can also supply our own custom implementation if we so desire:
use std::default::Default; struct Point { x: f64, y: f64, } impl Default for Point { fn default() -> Point { Point { x: 0.0, y: 0.0 } } } let point = Point::default();
Of course, this is trivial for this type, but you can see how it could be useful for more complex types.
The tradeoffs here are our usual suspects: Rust’s re-use of an existing concept/tool within the language (
trait
) vs. Swift’s use of syntax. Rust is slightly more explicit, making it obvious that a default value is being created—but Swift is perfectly readable and the syntax is consistent with many other languages, and it is shorter.Both languages use
.
syntax for member access. Swift:println("The point is: \(point.x), \(point.y)")
Rust:
println!("The point is {:}, {:}", point.x, point.y);
Swift lets you define items within a struct as mutable or constant. So you can create a variable struct instance, with some of its items immutable:
struct PointOnZAxis { var x: Double var y: Double let z = 0.0 } var point = PointOnZAxis(x: 4.0, 5.0) point.x = 5.0 point.y = 6.0 // This wouldn't compile, though: // point.z = 1.0
This is pretty handy for a lot of object-oriented programming approaches.
And Rust doesn’t have it. There are ways to accomplish the same thing; this isn’t the end of the world. Still, it’s an interesting omission, and it’s very much by design. Rust used to have this feature, and dropped it—and for good reason. Say you had a mutable field in a mutable struct, and then an immutable reference to it; should the mutable field be mutable, or immutable, with that reference?
The Rusty way to do this is to differentiate between public and private data. The above examples don’t make the public/private distinction particularly clear, because they assume everything is within the same module. However, many times, this will not be the case.
mod geometry { pub struct Point { x: f64, pub y: f64, } impl Point { pub fn new() -> Point { Point { x: 0.0, y: 0.0 } } pub fn set_x(&mut self, x: f64) { self.x = x; } } } fn main() { // Won't compile: the `x` field is private. // let mut p = geometry::Point { x: 0.0, y: 0.0 }; // Will compile: the `new` method is public. let mut p = geometry::Point::new(); // Won't compile: `x` isn't public. // p.x = 4.0; // You can use the setter, though: p.set_x(4.0); // You *can* set `y` directly, though, because it's public. p.y = 14.0; // You can't set fields either way if the instance is immutable. let q = geometry::Point::new(); // This fails because `set_x` requires a mutable reference, but `q` is // immutable. // q.set_x(4.0); // This fails because `q` is immutable, and so all its fields are, too. // q.y = 14.0; }
This is an interesting way of handling this issue. Rust takes the fairly standard use of information hiding (one of the basic principles of most object-oriented programming techniques) and combines it with the language’s normal mutability rules to make it so that the mutability of any given instance data is quite clear: all public members are just as mutable as the struct. If a member isn’t potentially publicly mutable, it isn’t publicly accessible. I really like this, though it took some mental readjustment.
There’s one other difference here, and it’s actually one of the areas Swift and Rust diverge substantially. Rust has
struct
for all product types; Swift splits them intostruct
types andclass
types.Swift
class
es have inheritance; there is presently no inheritance in Rust.Additionally, whereas Rust determines whether to use pass-by-reference or-value depending on details of the type (whether it implements the
Copy
trait
) and expected arguments to a function, Swift makes that distinction betweenclass
(pass-by-reference) andstruct
(pass-by-value) types. Quirky.Not bad, per se. But quirky.
Edit: I recently bumped into some discussion of data types in C♯ along with C, C++, and Java (here) and discovered that Swift is stealing this idea from C♯, which makes the same copy/reference distinction between
struct
andclass
.One consequence of this: in Rust, you’re always rather explicit about whether you’re accessing things by value vs. by reference. Not so in Swift; you have to remember whether the item you’re touching is a
struct
type or aclass
type, so that you can remember whether a given assignment or function call results in a reference or a copy. This is necessary because Swift doesn’t let you make that explicit (trying to hide the memory management from you). And it’s not alone in that, of course; many other high-level languages obscure that for convenience but still require you to think about it in certain circumstances. I’ve been bitten in the past by the value/reference distinction when thinking through the behavior of Python objects, for example, so that’s not a critique of Swift. Moreover, having the distinction betweenstruct
andclass
types does let you be more explicit than you might in e.g. Python about how given data will be handled.I won’t lie, though: I like Rust’s approach better. (Shocking, I know.)
All that nice initializer syntax for Swift
struct
types is absent for itsclass
types, which seems strange to me.Swift supplies some syntax for object identity, since it’s useful to know not only whether two
class
instances have the same data, but are in fact the same instance. You can use===
and!==
. Handy enough. To get at this kind of equivalence in Rust, you have to use raw pointers (which are often but not alwaysunsafe
; you can do this specific comparison without beingunsafe
, for example) to check whether the memory addresses are the same.