Rust and Swift (xiii)
Methods, instance and otherwise.
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 have methods which are attached to given data types. However, whereas Rust takes its notion of separation of data and functions rather strictly, Swift implements them on the relevant data structures (classes, structs, or enums) directly. In other words, the implementation of a given typeâs methods is within the body of the type definition itself in swift, whereas in Rust it is in an impl
block, usually but not always immediately adjacent in the code.
This goes to one of the philosophical differences between the two languages. As weâve discussed often in the series, Rust reuses a smaller set of conceptsâlanguage-level primitivesâto build up its functionality. So methods on a type and methods for a trait on a type are basically the same thing in Rust; theyâre defined in almost exactly the same way (the latter includes for SomeTrait
in the impl
expression). In Swift, a method is defined differently from a protocol definition, which weâll get to in the future. The point is simply this: the two take distinct approaches to the relationship between a given type definition and the implementations of any functions which may be attached to it.
Another important difference: access to other members of a given data type from within a method is explicit in Rust and implicit in Swift. In Rust, the first parameter to an instance method is always self
or &self
(or a mutable version of either of course), much as in Python. This explicitness distinction is by now exactly what we expect from the two languages.
Both use dot notation, in line with most other languages with a C-like syntax, for method calls, e.g. instance.method()
in Swift and instance.method()
in Rust. The latter is just syntactical sugar for T::method(&instance)
or T::method(instance)
where T
is the type of the instance (depending on whether the item is being borrowed or moved). Given its implicit knowledge of/access to instance-local data, and the distinctive behavior of Swift methods (see below), I donât think the same is, or even could be, true of Swift.
All of Swiftâs other behaviors around functionsâinternal and external names, and all the distinctions that go with thoseâare equally applicable to methods. Similarly, with the sole change that the first parameter is always the instance being acted on, a Rust methods follow all the same rules as ordinary Rust functions (which is why you can call the struct or enum method with an instance parameter as in the example above).
Swift does have a self
âit is, of course, implicit. Itâs useful at times for disambiguationâbasically, when a parameter name shadows an instance name. This will look familiar to people coming from Ruby.
The strong distinction Swift makes between reference and value types comes into play on methods, as you might expect, as does its approach to mutability. Methods which change the values in value types (struct
or enum
instances) have to be declared mutating func
. This kind of explicit-ness is good. As we discussed in Part 10, Rust approaches this entire problem differently: types are not value or reference types; they are either mutable and passed mutably (including as mut self
or &mut self
), or they are not. If an instance is mutable and passed mutably, a method is free to act on instance data. And in fact both languages require that the instance in question not be immutable. In fact, everything we said in Part 10 about both languages applies here, just with the addendum that private properties are available to methods.
The distinction, youâll note, is in where the indication that thereâs a mutation happens. Swift has a special keyword combination (mutating func
) for this. With Rust, itâs the same as every other function which mutates an argument. This makes Rust slightly more verbose, but it also means that in cases like this, the existing language tooling is perfectly capable of handling what has to be a special syntactical case in Swift.
Both Swift and Rust let you out-and-out change the instance by assigning to self
, albeit in fairly different ways. In Swift, youâd write a mutating method which updates the instance proper like this:
struct Point {
var x = 0.0, y = 0.0
mutating func changeSelf(x: Double, y: Double) {
self = Point(x: x, y: y)
}
}
In Rust, youâd need to explicitly pass a mutable reference and dereference it. (If you tried to pass mut self
instead of &mut self
, it would fail unless you returned the newly created object and assigned it outside.) Note that while the full implementation here is a couple lines longer, because of the data-vs.-method separation discussed earlier, the implementation of the method itself is roughly the same length.
pub struct Point {
pub x: f64,
pub y: f64,
}
impl Point {
pub fn change_self(&mut self, x: i32, y: i32) {
*self = Point { x: x, y: y };
}
}
Note that though you can do this, Iâm not sure itâs particularly Rustic. My own instinct would be to get a new Point
rather than mutate an existing one, in either language, and let the other be cleaned up âbehind the scenesâ as it were (with automatic memory management in Swift or the compilerâs automatic destruction of the type in Rust)âpurer functions being my preference these days.
You can do this with enum
types as well, which the Swift book illustrates with a three-state switch which updates the value type passed to a new value when calling its next()
method. You can do the same in Rust, with the same reference/dereference approach as above.
Hereâs a three-state switch in Swift:
enum ThreeState {
case First, Second, Third
mutating func next() {
switch self {
case First:
self = Second
case Second:
self = Third
case Third
self = First
}
}
}
And the same in Rust:
enum ThreeState { First, Second, Third }
impl ThreeState {
pub fn next(&mut self) {
match *self {
ThreeState::First => *self = ThreeState::Second,
ThreeState::Second => *self = ThreeState::Third,
ThreeState::Third => *self = ThreeState::First,
}
}
}
Both languages also have what Swift calls âtype methodsâ, and which you might think of as âstatic class methodsâ coming from a language like Java or Câ¯. In Swift, you define them by adding the static
or class
keywords to the func
definition. The class func
keyword combo is only applicable in class
bodies, and indicates that sub-classes may override the method definition.
struct Bar {
static func quux() { print("Seriously, what's a `quux`?") }
}
func main() {
Bar.quux()
}
In Rust, you simply drop self
as a first parameter and call it with ::
syntax instead of .
syntax:
struct Bar;
impl Bar {
pub fn quux() { println!("Seriously, what's a `quux`?"); }
}
fn main() {
Bar::quux();
}
As usual, Rust chooses to use existing language machinery; Swift uses new (combinations of) keywords.