Which (breaking) changes would you be willing to accept in a major language release? [+ Feature wishes]

Hey dear Swift community. I'd like to spawn a possibly interesting discussion, or rather a shared collection of opinions. I understand that this thread might re-hash some topics, but I find it still very much interesting if we could accumulate the data into a single spot, at least from the active members.

In this thread I would like to collect opinions on which breaking changes you would be willing to accept with a major language version release such as Swift 6.

:eyes: This thread is NOT about missing features such as variadic generics, generic protocols or anything else, just about the current set of features that you as a Swift developer would like to see to shift / change / receive a breaking change to make your usage of the language more enjoyable.

:warning: I welcome everyone to this thread to share their opinions, but keep in mind that the intention of this thread is to share your (possibly unpopular) opinion in a civil and productive manner and not to disrespect the opinions of other or worse turn this thread into another toxic place.

:+1: Overlapping opinions are very much welcome. They don't signal that you might have skipped some messages, but rather showcase that there is similar interests in some specific changes.

7 Likes

So here are my personal parts of the language I'd happily accept breaking changes in:

9 Likes

If one considered ABI breaking changes too, it would be interesting to consider how the standard library types might be implemented in today’s swift.
Like using opaque result types for different collection types in order to hide the implementation details that are ‘leaking out’ today.

8 Likes

I think this is a good idea but instead of collecting a wish list from the community, it might be best to ask the language group of what is currently on the docket for swift 6 for breaking change.

I'm not against this, but this should also not limit us sharing ideas for theoretical breaking changes regardless if they make it into Swift 6, Swift 7 or ever. :slight_smile:

4 Likes

Aside from literally everything @DevAndArtist listed in his wish list, I'd like:

  • Trailing closure syntax to not allow omitting the label.
  • Import statements to default to @_implementationOnly, which would be the same as internal import, where current unqualified import statement would be achieved with public import and @_exported import would be achieved by open import.
  • Remove Comparable requirement from Collection.Index In favor of index(_:precedes:) on the collection itself to enable a reasonable implementation of a linked list to conform to Collection (in which case index(_:precedes:) would have O(n) complexity, instead of the expected O(1)).
  • Remove global-scope private In favor of fileprivate (or private(file)) to avoid inconsistency and confusion.
  • Change the meaning of an unqualified protocol conformance clause to have internal access level (meaning that only code in the same module as the conformance declaration would see the conformance), where current behavior would be achieved with public conformance.
9 Likes

I’d be interested to see universal hashability for all types, including function types and using CustomHashable when default behavior needs to be overridden.

1 Like

How would you hash function types?

I would like to point out once more. I'd like this thread to focus on breaking changes or changes that has to be made in order to unlock something else in the future, rather than general missing features. Thanks everyone. :slight_smile:

4 Likes

If you meant to use the notation which I used then it'd be internal(file). ;)

Yes, that's right. :blush:

1 Like

A bit off-topic, but hashing types is already possible by converting their metatype into an ObjectIdentifier and getting the hash value from it. The more interesting part might be hashing a reference to a function / closure.

1 Like

I imagine something like this:

func hash(into hasher: inout Hasher) {
    hasher.combine(implementationPointer)
    for capture in captureGroup {
        hasher.combine(capture)
    }
}

one source-breaking change i would really like to see, is to be able to use the collection type sugar to refer to nested types:

// currently:
let index:Dictionary<Key, Value>.Index

// ideal:
let index:[Key: Value].Index
11 Likes

How is this source-breaking? Isn’t it a purely additive change?

You can treat every closure as a struct that carries unique identifier of the literal and all the captured variables. So you just need to compare/hash those.

From the first sight it may seem that you can use function pointer as unique identifier of the literal, but reabstraction thunks make things more complicated. You’d need to follow a chain of thunks until you find the original closure that can be used for comparison.

But even that might be not enough because of inlining. Then you’d need to compare metadata of the context objects as a stable identifier.

This may block some compiler optimizations and have other costs that need to be carefully evaluated, but overall is definitely possible.

This won't happen (force of habit, etc), but if I were to dream, here's a list of breaking changes:

  • make Int a typealias to Int32/64 (and seriously consider having it mapped to Int32 even on 64 bit devices)
  • make Float a typealias to Float32/64 and remove Double
  • make CGFloat a typealias to Float and remove all the hackery about CGFloat <-> Float interop.
  • alternatively (even better) make CGFloat a typealias to a new Fixed datatype.
  • remove NaN's (or expose the relevant functionality by some explicit additional APIs, so for every floating value "value == value" is true without exceptions).
  • change Optional.none to Optional.nil and remove bare nil
  • expose Bool as an enum with .false and .true and remove bare false / true
  • make enum values mutable (not entirely source breaking, can be made additive)
  • alternatively make struct values immutable (although that would be too radical).
  • change async from:
    func foo() async -> Int   to:
    async func foo() -> Int
  • change discardableResult from:
    @discardableResut func foo() -> Int   to:
    func foo() -> discardable Int
  • make "switch" and "do" statement expressions:
	func foo(x: Int) -> Int {
		switch x {
			case 1: 100
			case 2: 200
		}
	}

	func bar() {
		let x = do {
			123
		}
	}
  • change [:] to [] (use context to treat [] as empty dictionary when needed)

  • make commas optional e.g.:

    let items: [String: Int] = [
    	"first" : 1
    	"second" : 2
    	"third" : 3, "fourth" : 4 // comma is needed here
    	"fifth" : 5, // comma still allowed here
    ]

perhaps even here:

	foo(
		expression1
		expression2
		expression3
	)
  • allow types inference for parameter names:
    func foo(x = true) { ... }
  • allow types inference for function return type (additive):
    func foo() -> _ { 123 }
  • make closure parameters less ugly:
    { $0 * $1 }        --->
    { p0 * p1 } // or param0, or value0, etc, or this:
    { p[0] * p[1] } // or this:
    { p.0 * p.1 }

alternatively (even better) derive closure parameter names like so:

    func foo(execute: (_ a: Int, _ b: Int) -> Int) {
        ...
    }
    ....
    foo { a * b }
  • change autoclosure from:
	func foo(x: @autoclosure () -> Bool) -> Bool {
	    x() // parens
	}
	
	to

	func foo(x: lazy Bool) -> Bool {
		x // no parens
	}
  • change && to & and || to | (they will still "short-circuit" for Bool ops)
  • change Bool's not ! to something else like suffix .not and change != to <>. Use pling only to denote "optional unwrapping" business (something that can terminate the app).
  • while on this: check all current places that can terminate the app and introduce either a pling or a try in there, examples:
func foo() {
    let a = Dictionary(uniqueKeysWithValues: [(1, 1), (1, 1)]) // prohibit
    let a = Dictionary(uniqueKeysWithValues: [(1, 1), (1, 1)])! // either this
    let a = try! Dictionary(uniqueKeysWithValues: [(1, 1), (1, 1)]) // or this

    let b: [Int: Int] = [1:1, 1:1] // prohibit
    let b = [Int: Int]([1:1, 1:1])! // either this
    let b = try! [Int: Int]([1:1, 1:1]) // or this
    
    let c: Set<Int> = [1, 1] // prohibit
    let c = Set<Int>([1, 1])! // either this
    let c = try! Set<Int>([1, 1]) // or this
    
    let array: [Int] = [1, 2, 3]
    let d: Int = array[123] // prohibit
    let d: Int = array[123]!
}
  • change -> to :

    func foo() -> Int { ... }
    func foo(): Int { ... }

  • make subscript label rules compatible to function label rules

    subscript(_ internalName: Int)  --> allow only: foo[1]
    subscript(name: Int)            --> allow only: foo[name: 1]
    subscript(externalName x: Int)  --> allow only: foo[externalName: 1]
8 Likes

One big one that I’d love to see (but will probably never happen as it’s would change pretty much all Swift code): completely change the syntax for protocol conformance. I would heavily argue that Objective-C did it far better where concrete types and protocols were separate (e.g. whereas in Swift you’d do class Foo: Superclass, Protocol1, Protocol2 {}, in Obj-C you’d have @interface Foo : Superclass <Protocol1, Protocol2>). This improves readability and also gets around several issues where a protocol and a concrete type can have the same name, as they are now used in different contexts so the compiler can easily differentiate

6 Likes

Remove the Any and AnyObject classes in favour of modern alternatives.

7 Likes
  • Finally fix the static protocol extension dispatch footgun
  • Deterministic retroactive conformance resolution
  • Require protocol conformance marker on implementing methods similar to override (disambiguate conformance, clean up old methods)
  • Replace stringly-typed callAsFunction with a proper attribute
  • Eliminate fileprivate, and go back to the drawing board on access control
  • Internal imports by default
  • Undo some of the SwiftUI purpose-built "magic": omit return, multiple trailing closures, result builders
  • Pure functions by default
  • Disambiguate protocol conformance and inheritance
6 Likes