Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I can’t speak for the author, but I’ve had a similar feeling. Where (safe) Rust’s core principle is memory safety at all costs, I feel that Swift takes a more nuanced approach, prioritizing making good programming patterns easier while still being memory safe (though perhaps not as expressive as Rust).

Swift also is much more principled when it come to certain things like exceptions. Whereas Rust (if I remember correctly) allows arbitrary panic-ing and catching of panics, Swift forces you to acknowledge a possible panic at function call sites using “try”. Without “try” control flow can’t suddenly end.

Admittedly, I haven’t used Rust significantly in a couple years, but I wrote this a few years back and some of the points still have merit: https://gist.github.com/GeorgeLyon/c7b07923f7a800674bc9745ae...



> Whereas Rust (if I remember correctly) allows arbitrary panic-ing and catching of panics

While it is possible to "catch" a panic in Rust, this is not an error-handling strategy, it is a correctness fallback. The intent is that, if you're writing a Rust library that exposes a C ABI, you halt the panic at the boundary of your library, because there is no standardized cross-language unwinding scheme and so unwinding across an FFI boundary would be UB. It's not there for error handling (and it can't be, not only because the API is deliberately designed to discourage it, but also because Rust programs can be compiled in a mode where panics abort instead of unwind).

> Swift forces you to acknowledge a possible panic at function call sites using “try”. Without “try” control flow can’t suddenly end.

This is a bit inaccurate. You can absolutely do things like divide by zero in a Swift function that isn't marked as `try`, which will crash the program. This is the same behavior that is expected of panics in Rust.


> while still being memory safe (though perhaps not as expressive as Rust).

Rust's memory safety protects against data races. AFAIK, Swift does not.

> Swift also is much more principled when it come to certain things like exceptions. Whereas Rust (if I remember correctly) allows arbitrary panic-ing and catching of panics, Swift forces you to acknowledge a possible panic at function call sites using “try”. Without “try” control flow can’t suddenly end.

Generally in Rust, a panic means the program has entered a state that is unexpected by the programmer. One example is an unexpected index out of bounds error.

For cases where an error is an expected program state, like a web request or input validation, it's recommended to use `Option` or `Result` instead. Rust will force you to handle the error case.

Yes, any function can panic, and any caller can catch panics, but panics are not the primary "exception" analogue in Rust. Panics are only for fatal errors.

I don't know anything about Swift, but from a quick search I see something called "Fatal Errors", which I assume act a similar way.

Now to address the points in your gist.

> lauded the Borrow Checker for discouraging people from using the borrow checker! This results in less performant code much of the time.

The borrow checker is much improved with non-lexical lifetimes which makes it far easier to deal with.

> 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

I have no idea how you could, for instance, implement something like `serde` without macros or reflection without building it into the language. Other common uses, like `lazy_static`, have been replaced with better APIs that don't use macros at all. Building these things into the language or standard library without enough experimentation beforehand can be problematic, as `lazy_static` shows.

Rust's development is more open than Swift. It doesn't have the luxury of a single owner that can dictate THE WAY to do things, because it must support use cases from web servers to embedded MCUs. Macros and other forms of build-time execution provide that flexibility.

> Coarse Imports

Personally, I think Rust's module system and imports area the best I've ever used. As I said, I've never used Swift. Are all of the standard library functions in a single namespace?

I imagine that this makes implementing things like refactoring much easier.

> First party IDE > > Say what you want about Xcode

Xcode is MacOS exclusive.

> With the current state of RLS, developers often forsake modern IDE features even though reliable code completion, refactoring tools and diagnostics greatly benefit developers.

RLS is now deprecated, and rust-analyzer is fantastic.

> Out-of-the-box debugging experience

I haven't ever tried interactive debugging with Rust, so I can't comment.

> One of the more powerful uses of a type system is to have functions perform different processing based on the type of its arguments.

I think one of the fundamental difference of today is that this should be handled by traits, not by standalone functions.

> 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.

I don't think this is a common pattern at all. Generally you'd just implement the trait for each type and put the different behaviors in the trait impl, rather than in the function using an enum.

> 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.

This is only true for types outside your crate.

> 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)) -> ();
    }
I think what you're looking for is:

    trait Foo: Bar + Baz {
        fn foo(&self);
    }
This will work as long as Bar and Baz are object-safe.

> 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) -> () { … }
You can make the trait generic instead of the function:

    trait Foo<T: Bar + Baz> {
        fn foo(&self, t: T);
    }
> Multiple if let cases

They are a work in progress, available in nightly: https://github.com/rust-lang/rust/issues/53667

But they do require an extra level of parens in some cases, though I think that's insignificant.

> Guard Statements

Rust has these now as "let ... else" statements.

    let Pattern(binding) = thing else { ... };
> Explicit try

This was covered above: use Result or Option instead. Panics are not exceptions.

And as for unwrap, yeah you just have to know that it will panic. It's just a thing you have to know about the language, like `try`.

> Rethrows

Transpose is stable now. But generally for that case I'd do this instead:

    let y = Some(1).ok_or(Error)?.and_then(|i| {
        if i > 3 {
            Err(Error)
        } else {
            Ok(i + 1)
        }
    })?;
> Ternary Operator

I prefer the lack of a ternary, since, as you show, an if can do the same thing.

> String Interpolation

You can include variable names inline now. And they're not C-style at all.

> Named arguments

Personally, I've never felt the lack. Especially with the inline hints from rust-analyzer.

> Default arguments

I actually prefer how Rust does not have function overloading. This includes default arguments and optional arguments.

> Function-level polymorphism

And the above also applies to this.

> This is also useful for operators (in Swift, + is a polymorphic function)

This is implemented with traits in Rust.

> Alternatively, you could use use:

    use MyDescriptiveEnum as E;
    match e {
        E::Foo => …,
        E::Bar => …,
    }
You can do a glob import instead

    use MyDescriptiveEnum::*;
    match e {
        Foo => …,
        Bar => …,
    }
> Raw types for enumerations

Rust enums have this for primitive numeric types, but others do not.

> Rust's enum and struct initializers all use different syntax. In Swift, there is a single syntax

Sure, they both use parens, but they are still different syntaxes.

> Trailing Closures

I don't like this. Seems like a special case for little to no reason.

> Default Closure Arguments

Same as the above.

> Extensions, Type Inference for Associated Types

I agree these could be useful improvements.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: