Note This is a little out of date. Rust has made some progress on some points, however many points still apply.
Swift shares Rust's enthusiasm for zero-cost abstractions, but also emphasizes progressive disclosure. Progressive disclosure requires that language features should be added in a way that doesn't complicate the rest of the language. This means that Swift aims to be simple for simple tasks, and only as complex as needed for complex tasks.
Unlike Rust, which requires everything to be explicitly specified, Swift recognizes that the compiler is a powerful tool that can make good decisions on your behalf. Rust's hard edges discourage developers from using its more powerful features. In fact, the closing keynote to RustConf 2018 lauded the Borrow Checker for discouraging people from using the borrow checker! This results in less performant code much of the time. Even though Rust is capable of producing very performant code, I'd wager that Swift code is often more performant simply because the compiler is designed to optimize the abstractions that developers actually use.
The escape hatch of macros and build-time source generation has allowed Rust to sweep some of its usability issues under the rug (Just use a macro!). Swift strives to support all of the use cases developer have in the language itself, to the point where if you are reaching for something like source generation you are probably doing something wrong. Over time, this has caused Swift to become much more flexible and feel natural in many domains.
In Rust, imports are extremely fine-grained. I'm not sure why this is the case, or what benefit this provides. In Swift, you can import the full standard library with just import Foundation
. Compiler features like dead code elimination make this fairly low cost.
Swift has a focus on security. NPM
is currently the punching bag of package managers with security issues, but there is nothing fundamentally different about cargo
with the possible exception that it is currently being a much lower value target.
Say what you want about Xcode (believe, me I have more war stories than most), but in its current iteration it is a stable, performant, standard development environment with many features missing from the VSCodes and Sublimes of the world. With the current state of RLS, developers often forsake modern IDE features even though reliable code completion, refactoring tools and diagnostics greatly benefit developers.
LLDB is deeply integrated into Xcode, has thorough Swift support, and works out of the box.
In Rust, if you call a function foo()
, it resolved which foo
you meant and performs type checking only after resolving the function.
In Swift, a function call foo()
will resolve to a set of function you may be referring to. From there, intuitive and deterministic rules select which specific function implementation to use.
Below, I've listed a few advantages of Swift which result from this distinction. Rust is currently trying to address this problem piecemeal, but I think the ship has sailed in terms of adopting a more general solution that works in all cases.
One of the more powerful uses of a type system is to have functions perform different processing based on the type of its arguments.
In Rust, this is achived via the From
and Into
mechanic.
The first unfortunate consequence of this is that you often have to create a new type and trait pair per argument you want to be polymorphic. This includes quite a bit of boilerplate:
pub enum Argument {
BehaviorA(i64),
BehaviorB(f64),
}
pub trait IntoArgument: Sized {
fn into_argument(self) -> Argument;
}
impl IntoArgument for f64 {
fn into_argument(self) -> Argument { … }
}
impl IntoArgument for i64 {
fn into_argument(self) -> Argument { … }
}
fn polymorphic<T: IntoArgument>(t: T) }
let argument = t.into();
…
}
This become MUCH more complicated when traits are involved, because you can effectively only ever have one generic implementation of your custom into Into…
for a trait, since otherwise the type checker complains there can at some point in the future be a collison. For instance, the following implementations will conflict, because there may be a type that is both Into
and Iterator
:
impl<T: Iterator<Item = f64>> IntoArgument for T { … }
impl<T: Into<f64>> IntoArgument for T { … }
In Swift, the function is selected at the call site based on the types of its arguments. Functions are also selected in order of increasing generality, and you can always select the specific function to call by casting the argument.
fn polymorphic(_ floats: [Float]) { … }
fn polymorphic(_ ints: [Int]) { … }
fn polymorphic<T: Sequence>(_ int_sequence: T) where T.Element == Int {
polymorphic(Array(int_sequence));
}
// calls the `[Int]` variant
polymorphic([3])
In Rust, you cannot create trait objects out of certain types of traits. Often, you could use trait objects for this type of thing, but I haven't figured out a way to do this with more complex constraints. For instance:
trait Bar {}
trait Baz {}
trait Foo {
// This is invalid
fn foo(t: &(Bar + Baz)) -> ();
}
Making it generic disallows using Foo
as a trait object:
trait Foo {
fn foo<T: Bar + Baz>(t: T) -> ();
}
// This is invalid
fn do_work(f: &Foo) -> () { … }
In Swift, protocols can just have generic methods. Under the hood, the function is called with the equivalent of a trait object.
protocol Foo {
// This is fine
func foo<T: Bar, Baz>(t: T) -> ()
}
In Rust, if you want to bind multiple arguments in an if let
you have to use a tuple:
if let (Some(a), Some(b)) = (option_a, option_b) {
…
}
In Swift, you can write multiple let statements together:
if let a = optionA,
let b = optionB
{
…
}
This becomes even more powerful when you include conditions:
if let a = optionA,
a > 3,
let b = optionB
{
…
}
Even without let
bindings, the fact that the ,
has the highest precedence makes it great for multiline expressions:
if a || b,
c || d
{
…
}
In Rust, this would be expressed like so:
if (a || b) &&
(c || d)
{
…
}
Rust's representation becomes significantly more complicated as more expressions are added.
As part of the effort to combat deep nesting, Swift introduced guard
which is like an if
statement where the body must return
or throw
, and binds values to the outer scope:
guard let a = option_a else {
return
}
a.do_stuff()
In Swift, a function or method cannot panic unless it is explicitly marked as throws
. In Rust, anything can panic()
. In complex scenarios, invariants can be broken in subtle ways when arbitrary statements panic.
Rust: i64::parse(s).unwrap();
(no indication of a potential panic, except knowing that unwrap
can panic)
Swift: try Int(s)
Swift will also force you to handle the error, which you could conveniently convert to an Option with try?
:
do {
try Int(s)
} catch e {
…
}
In Swift, you can define a function which throws only if a closure argument throws. map
is a good example:
// Simple case
let x = [1].map { $0 + 1 }
// Requires `try`
let y = try [1].map { i in
if i > 3 {
throw Error
} else {
i + 1
}
}
In Rust, the latter case requires using collect
for Vec
, or the unstable transpose
for Option
vec![1].iter()
.map(|i|
if i > 3 {
Err(Error)
} else {
OK(i + 1)
})
.collect::<Result<Vec<_>,_>()?
#![feature(transpose_result)]
Some(1).map(|i|
if i > 3 {
Err(Error)
} else {
OK(i + 1)
})
.transpose()?
Swift: let x = a > b ? a : b
Rust: let x = if a > b { a } else { b }
Along with overloading operators, Swift allows defining custom operators. This is particularly useful when creating DSLs.
Rust uses old C-style format strings (albeit with inferred types), which can get a little unweildly:
println!("{} some text {} some more text {} more text {}: {}", a, b, c, d, e);
In Swift, interpolations are included inline:
print("\(a) some text \(b) some more text \(c) more text \(d): \(e)")
This is also about to get a whole lot more useful for custom types (like SQL expressions)
Rust has discussed this already but decided to postpone. Its my opinion that named arguments make code much more readable at the point of use.
- Rust:
dog.walk(…, true)
- Swift:
dog.walk(…, onLeash: true)
Rust has discussed this already but decided that while it is "oft-requested") because they can do it later, they are not going to do it.
In Swift, one can:
func walk_dog(onLeash: Bool = false) { … };
walk_dog();
walk_dog(onLeash: true);
This was covered in a previous section, but it also affects the usability of the language. In Rust, a function name is its identity. In Swift, the compiler takes into account the type of the arguments.
Rust:
let dog: Dog = ...;
let cat: Cat = ...;
fn walk_dog(dog: Dog) { … }
fn walk_cat(cat: Cat) { … }
walk_dog(dog);
walk_cat(cat);
Swift:
let dog: Dog = ...
let cat: Cat = ...
func walk(_ dog: Dog) { … }
func walk(_ cat: Cat) { … }
walk(dog)
walk(cat)
This is also useful for operators (in Swift, +
is a polymorphic function)
func +(lhs: Int, rhs: Int) -> Int { … }
func +(lhs: Float, rhs: Float) -> Float { … }
Swift's compiler can figure out what namespace you mean, which is useful in switch
/match
.
Rust:
enum MyDescriptiveEnum {
Foo,
Bar
}
let e: MyDescriptiveEnum = …;
match e {
MyDescriptiveEnum::Foo => …,
MyDescriptiveEnum::Bar => …,
}
call_fn(MyDescriptiveEnum::Foo);
Alternatively, you could use use
:
use MyDescriptiveEnum as E;
match e {
E::Foo => …,
E::Bar => …,
}
Swift:
enum MyDescriptiveEnum {
case foo, bar
}
let e: MyDescriptiveEnum = …
switch e {
case .foo:
…
case .bar:
…
}
call_fn(.foo);
In Swift, you can specify a raw type for enumerations, which synthesizes init?(rawValue: Raw)
and var rawValue: Raw
. In Rust you would need a #[derive(…)]
directive or two match
statements.
Rust's enum and struct initializers all use different syntax. In Swift, there is a single syntax:
Rust:
let e_1 = MyEnum::X(0);
let e_2 = MyEnum::Y{ name: 3 };
let s = MyStruct{ x, y: 3 };
Swift:
let e1 = MyEnum.x(o)
let e2 = MyEnum.y(name: o)
let s = MyStruct(x: x, y: 3)
In Swift, if the last argument to a function is a closure, it can be added after the close paren. If it is the only argument, the parens can be omitted entirely
// async(_ block: @escaping () -> ()) is a function on DispatchQueue
queue.async {
…
}
// asyncAfter(delay: DispatchTimeInterval, _ block: @escaping () -> ()) is a function on DispatchQueue
queue.asyncAfter(.seconds(3)) {
…
}
A small, but usefull feature Swift appropriated from scripting languages is default closure arguments: [1].map({ $0 + 3 })
In Rust, if you want to add functionality to a type which is defined outside your current crate, you need to either wrap it or create a custom trait. If you want to add that functionality to multiple types, it gets a little messy:
trait MyAdditionalFunctionality<'a, T: Constraint> {
fn do_work();
}
impl<'a, T: Constraint> MyAdditionalFunctionality<'a, T> for f64 {
fn do_work() { … }
}
impl<'a, T: Constraint> MyAdditionalFunctionality<'a, T> for i64 {
fn do_work() { … }
}
In Swift, you can simply create an extension:
extension Float {
func do_work() { … }
}
extension Int {
func do_work() { … }
}
In Rust, associated types must always be explicitly set in the implementation:
impl Iterator for MyStruct {
type Item = i64;
fn next() -> Option<i64> {
…
}
}
In Swift, the type checker can infer them:
extension MyStruct: IteratorProtocol {
// `typealias Item = Int` is implicit
func next() -> Option<Int> {
…
}
}
In Swift, you can define custom patterns using the ~=
operator, which you can then use in switch
func ~=(value: Int, pattern: MyCustomPattern) -> Bool {
…
}
switch 5 {
case MyCustomPattern():
…
default:
…
}