Skip to content

Instantly share code, notes, and snippets.

@GeorgeLyon
Last active July 2, 2024 15:45
Show Gist options
  • Save GeorgeLyon/c7b07923f7a800674bc9745ae45ddc7f to your computer and use it in GitHub Desktop.
Save GeorgeLyon/c7b07923f7a800674bc9745ae45ddc7f to your computer and use it in GitHub Desktop.
A list of advantages Swift has over Rust

Note This is a little out of date. Rust has made some progress on some points, however many points still apply.

Philosophy

Progressive disclosure

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.

The compiler works for you

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.

No macros / build-time code generation

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.

Package Management

Coarse imports

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.

No build-time code execution

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.

Tooling

First party IDE

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.

Out-of-the-box debugging experience

LLDB is deeply integrated into Xcode, has thorough Swift support, and works out of the box.

Update: I don't generally aim to keep this file up to date, but I've been using VSCode for Swift development for a few years now and it is great, especially when using devcontainers, so I felt I needed to tip my hat and update this section.

Type System

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.

Function-level polymorphism

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])

Generic methods in protocols/traits

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) -> ()
}

Sugar

Multiple if let cases

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.

Guard Statements

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()

Explicit try

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 {
    …
}

Rethrows

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()?

Ternary Operator

Swift: let x = a > b ? a : b Rust: let x = if a > b { a } else { b }

Custom Operators

Along with overloading operators, Swift allows defining custom operators. This is particularly useful when creating DSLs.

Named arguments

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)

Default arguments

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);

Function-level polymorphism

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 { … }

Implicit Namespacing

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);

Raw types for enumerations

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.

Custom Initializers

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)

Trailing Closures

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)) {
    …
}

Default Closure Arguments

A small, but usefull feature Swift appropriated from scripting languages is default closure arguments: [1].map({ $0 + 3 })

Extensions

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() { … }
}

Type Inference for Associated Types

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> {
        …
    }
}

Custom Patterns

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:
    …
}
@DrJume
Copy link

DrJume commented Jun 6, 2023

Thank you!

@fh2ctm
Copy link

fh2ctm commented Jul 21, 2023

Very nice read.

@PEZO19
Copy link

PEZO19 commented Jul 27, 2023

Thanks! :)

@xzbdmw
Copy link

xzbdmw commented Sep 27, 2023

trait F:Bar+Baz{}
 impl<T> F for T where T:Bar+Baz
trait Bar {}
trait Baz {}
trait Foo {
    // This is invalid
    fn foo(t: &F) -> ();
}``` 

@xzbdmw
Copy link

xzbdmw commented Sep 27, 2023

let e_2 = MyEnum::Y{ name: 3 };
let s = MyStruct{ x, y: 3 };```
this is the same thing. MyEnum::Y can be treated as a struct and the same as struct initialization syntax

@coldjugular
Copy link

No macros / build-time code generation

Swift has since added macro support in version 5.9, released 2023-09-18.

@rhzs
Copy link

rhzs commented Mar 3, 2024

just quick observations, if you complaint the X language is more verbose than Y language, I don't think it is fair comparison. The language has its own purpose and excels at certain things. You should be more fair in your judgment, please do see people raising a lot of complaints about its stability in swift forum itself, especially when building for server dev. The pain of upgrading one language version to another level language version can't be taken lightly. This can indicate that serious fundamental problem with the language itself, where the creator doesn't have clear direction on the language vision.

@GeorgeLyon
Copy link
Author

GeorgeLyon commented Mar 3, 2024

Swift has since added macro support in version 5.9, released 2023-09-18.

You are correct. I will say that Swift’s macro implementation is nicer than what I’ve seen in other languages. It is typechecked before invoking the macro as well as after and there are limits to what a macro can do and API to specify them (I.e. this macro adds a function called “foo”) which makes dealing with macro code much saner and compatible with tooling.

The bigger problem with recent Swift is build plugins. While they were implemented for simple tasks like generating Swift code from a protobuf file, there are very few guardrails and folks have already begun to misuse them in annoying ways. Also since build plugins are just executable programs there really isn’t any structured way to reason about them.

please do see people raising a lot of complaints about its stability in swift forum itself, especially when building for server dev

I browse the Swift forums quite often and haven’t seen this sentiment. Is there something specific you are referring to? SwiftNIO is reasonably stable and powers a great many server-side applications.

The pain of upgrading one language version to another level language version can't be taken lightly

All Swift 5.X versions have been mostly source-compatible. Swift 6 will break this but this has been forecast for years and there are a variety of tools planned to help migrate. It is also a good thing I feel to have infrequent source compatibility breaks with a clear migration path, as it allows you to clean up mistakes and address how the language has evolved (like Swift 6’s new enforcement of any and some protocol qualifiers).

This can indicate that serious fundamental problem with the language itself, where the creator doesn't have clear direction on the language vision.

I’m not sure what you mean by this, Swift’s Core Team does a pretty good job of keeping the language internally consistent. Also, sorry to be a bit of a troll but it seems Rust is the language with a known fundamental soundness hole in its type system.

For the record, this doc is pretty old and was mostly put together to aid in convincing folks Swift is a serious systems language, suitable for server development and other systems tasks, and to highlight Swift’s approach to usability that I think other languages can learn from. The goal was not to put down Rust.

@rhzs
Copy link

rhzs commented Mar 4, 2024

The goal was not to put down Rust.

I am not starting the debate. But your gist introduction content is too bias, that's why i wrote my objection.

I browse the Swift forums quite often and haven’t seen this sentiment. Is there something specific you are referring to? SwiftNIO is reasonably stable and powers a great many server-side applications.

Here I am not nitpicking on specific issue. this is just one example why swift is not great for linux derivatives and other embedding ecosystems. Wagering that swift is more performant? I may agree, perhaps only within Apple's ecosystem.

All Swift 5.X versions have been mostly source-compatible. Swift 6 will break this but this has been forecast for years and there are a variety of tools planned to help migrate. It is also a good thing I feel to have infrequent source compatibility breaks with a clear migration path, as it allows you to clean up mistakes and address how the language has evolved (like Swift 6’s new enforcement of any and some protocol qualifiers).

I’m not sure what you mean by this, Swift’s Core Team does a pretty good job of keeping the language internally consistent. Also, sorry to be a bit of a troll but it seems Rust is the language with rust-lang/rust#57893 (comment).

I don't think the sentence: a pretty good job of keeping the language internally consistent is correct. Again, this is a proof the bias that you made. I agree with you on Rust design, that may require breaking solution as well.

@foobra
Copy link

foobra commented Mar 6, 2024

Anything for Memory safety or Concurrent safety to share? Thanks

@pietervandermeer
Copy link

String interpolation in Rust was added a while ago. Strict imports are actually more of a blessing than anything else. Especially when scaling up. Otherwise a valid comparison. Rust just has more clutter. At the added benefit of having no garbage collector or reference counting, I'll take it, though.

@GeorgeLyon
Copy link
Author

GeorgeLyon commented Apr 3, 2024

At the added benefit of having no garbage collector or reference counting, I'll take it, though.

@pietervandermeer I'd call Swift's reference-counting "somewhat optional" (and there is no garbage collector). For your own code, you can just use structs and enums and no reference counting will occur. For standard library types, some of them are implemented with reference-counted storage under the hood, but are generally well optimized. I would argue Rust has effectively the same model with Arc (you can choose not to use it in your code; some things are implemented using Arc under the hood). Swift just elevates this to a language feature because it simplifies higher-level code.

Also, there is ongoing work in Swift to create an "embedded mode" which would further constrain what the runtime can do.

@pietervandermeer
Copy link

pietervandermeer commented Apr 3, 2024 via email

@xzbdmw
Copy link

xzbdmw commented Apr 4, 2024

swift lsp is slow and can't even step into stdlib code, language is good but tools awful.

@quackerex
Copy link

Ternary Operator

Swift: let x = a > b ? a : b Rust: let x = if a > b { a } else { b }

I don't think ternary operators are necessary good thing as they are harder-to-read. Even swift agrees

Use the ternary conditional operator with care, however. Its conciseness can lead to hard-to-read code if overused. Avoid combining multiple instances of the ternary conditional operator into one compound statement.

However with Swift 5.9 now allows you to use if and switch as expressions.

statusBar.text = if !hasConnection { "Disconnected" }
                 else if let error = lastError { error.localizedDescription }
                 else { "Ready" }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment