Swift 6.4 Released
16 points by kevinc
16 points by kevinc
More natural optional some and any types. When writing an optional some or any type, you no longer have to wrap the type in parentheses. Instead of (some Rocket)?, you can simply write some Rocket? (SE-0521).
It's fun to follow Swift. As a fan of Rust, Swift feels similarly modern, while being significantly less principled, and more open to adding all sorts of kitchen sink convenience features.
(See also, the whole issues caused by adopting type inference and subtyping, causing exponential blowup and time outs on type checking one line programs.)
I am really happy that Rust avoided the fate so far, but it's also fun to have a point of comparison.
Rust has both type inference and subtyping too :-) but restricts them (function-local type inference, subtyping of references).
Sigh, I got Anubis choosing to eat my comment, so this is very very abbreviated
The core difference is overloading on return type. More or less every other language disallows this - there is a single return type for any given parameter types. this applies to generic functions as well: even though different instantiations of a function may have different return types, there's only a single instantiation of the function (and thus return type) for a given set of parameter types.
e.g.
fn foo(arg:u32)->u32; fn foo(arg:u32)->String;
is not valid, but in swift
func foo(arg:Int)->Int { return arg; }
func foo(arg:Int)->String { return String(arg); }
let x = foo(1) + 1; // x is int
let y = foo(2) + "hello"; // y is String
let z = foo(arg:foo(arg:1)); // ambiguous
let z1 = foo(arg:foo(arg:1)) + 1; // it's an Int
let z2 : Int = foo(arg:foo(arg:1)); // it's an Int
let z3 = foo(arg:foo(arg:1)) + "hello"; // it's a string
let z4 : String = foo(arg:foo(arg:1)); // it's a string
You can see how combining these in different ways could start leading to a very large set of possible call structures
(Disclaimer: I am not trying to evangelize Rust here. I don't say Rust is better than Swift.)
The core difference is overloading on return type.
I don't think that can be so, because Rust is also one of the unusual languages to allow overloading on return type, although Rust makes overloading of any kind clunky to discourage using it heavily.
core::convert::Into::into is an example of a function in the Rust standard library that is overloaded on its return type and typically is used with the return type inferred, as x.into() with some arbitrary x.
Here is your return type overloading example converted to Rust('s clunky overloading), runnable in the Rust Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=1591c1e8e4634aa2e5dfc2eae3408840
Rust rejects (refuses to infer types for) each of the foo(...) + ... expressions, but wrapping the addition operations in more explicit function calls bypasses this.
Oh right, that's how .into() works - please hold while I work out the exact problem.
I wonder if it's specific knowing at least one side statically. Please hold.
@5d22b Ah right - this took me a moment to recall: rust does not allow ad hoc overloading.
e.g. I forgot entirely that rust does not actually allow
fn foo(arg:u32)->u32;
fn foo(arg:f32)->f32;
It took me some futzing around on godbolt to restore my understanding of all of this.
So something like into() has a single definition, if we look at a generic function I think this is equivalent to something like fn thing<T>(arg:i32)->T where you can then have foo(thing(arg:1)) and there is a single type for foo, so there's a single candidate you're trying to bind T to. But in swift you can have multiple definitions of foo that have different parameters types so there are multiple different inference paths. e.g func thing<T>(arg:i32)->T but also distinct func thing(arg:Int)->Float, and func thing(arg:String)->Blargh.
The core issue is that rust only allows a single function named F with a single type, whereas swift allows many.
So when rust says "what is the type of F?" there is one answer, for swift there can be multiple.
Oh, to put this more clearly, you can do:
pub trait Into<T>: Sized {
// Required method
fn into(self) -> T;
}
fn foo(i32)->String
let x = foo(someInto.into());
but consider how that would resolve if you could also have
fn foo(f32)->String
now someInfo.into() can't be inferred.
I'm aware, yeah. The situation of why Swift ran into that issue is a bit more complicated, I didn't want to retell it here.
I think when I researched it I viewed it as a problem arising from HMTI (so some sort of constraint solving) + a situation where the compiler is allowed to pick "one out of several options" at many nodes in the graph, resulting in it having to explore exponentially many paths. Subtyping and implicit conversions make this really easy iirc.
If I understand what you mean, Swift also restricts type inference within function boundaries—after func, you must specify the types of all terms in the signature. The blowup comes from the search branching factor when we have type-distinguished overloading, especially of operators, which are free functions with common names. I think Rust was considering adding that feature, and if so I might warn against it.