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

There are bunch of great and intriguing ideas in the thread so far! Here's some from my wishlist...

  • Closed protocols (ie, protocols that are public but cannot be adopted by anyone). There are times when I want to express that some types are all related, but I intentionally do no support anyone else making a related type. CKRecordValueProtocol is one such protocol that would benefit from this, IMO.
  • public import, internal import, and private import.
  • Overhaul access control stuff so I can finally do things like typeprivate, public-and-open-but-only-for-subtypes, friend, etc
  • Death to fileprivate
  • also: module CustomModule { ... }
  • add the !! and ?! operators (ie "crash if nil" and "throw if nil", respectively). These are ridiculously helpful when doing custom decoding logic:
    init(from anyJSON: Any) {
        // before:
        guard let json = anyJSON as? [String: Any] else { 
            fatalError("Getting anything except a dictionary is a logic bug") 
            // yes, I can use `as!` here, but if I have a way to give a custom error message,
            // I'd like to provide one
        }
        guard let value = json["key"] as? String else { throw SomeJSONError... }
        self.value = value
    
        // after:
        let json = anyJSON as? [String: Any] !! "Getting anything except a dictionary is a logic bug"
        self.value = try (json["key"] as? String) ?! SomeJSONError...
    }
    
  • complete interoperation between throws methods and result methods. Example:
    // given
    func myThrowingFunction() throws -> Foo { ... }
    // this should be callable as either:
    let result = myThrowingFunction() // "result" is a Result<Foo, Error>
    // or (currently)
    let foo = try myThrowingFunction() // "foo" is a Foo
    
    ... perhaps for async methods as well, although I haven't thought about that as much.
  • typed throws
  • a lot more synthesis around enums (isCase: Bool and var casePayload: PayloadType? accessors)
  • deprecate and replace Codable
4 Likes

:heart:

7 Likes

+1000. the current Codable design is fine for infrequently-called functionality, but the performance is just terrible for high-throughput use cases.

7 Likes

There are a lot of ideas in this thread that I both agree and disagree with, but this has been one of my #1 feature requests for a long time.

I wish try was applied after performing functions on Result so method chaining could express behavior like:

let value = try doFoo()
  .logError(logger) { "Failed to do bar: \($0)" }
let value = try doFoo()
  .replaceError(fallbackValue)

In a similar vein, I wish try was expressible in a library so similar control flow keywords could be rewritten for Optional and Bool. ex:

unwrap returnsOptional() // returns nil from the calling function if returnsOptional returns nil 
require returnsBool() // returns false from the calling function if returnsBool returns false

This could enable code like:

require throwingFn()
  .logError ...
  .isSuccess
1 Like

:100: from me.

It's a bit less succinct as in your example but we can do this already:


let fromThrowingToResult = Result { try foo() }
let fromResultToThrowing = try! bar().get()

func foo() throws -> Int {
    if (x & 1) == 0 {
        throw NSError(domain: "", code: -1, userInfo: nil)
    } else {
        return x + 1
    }
}

func bar() -> Result<Double, Error> {
    if y < 0 {
        return .failure(NSError(domain: "", code: -1, userInfo: nil))
    } else {
        return .success(sqrt(y))
    }
}

Do you think it can be optimised in the current form without complete rehaul / deprecation / replacing, or is there some inherent drawback deep in its design that makes it slow?

Note, that if you write it on a single line it becomes:

try doFoo().logError()
try doFoo().replaceError()

which shows the need for parens:

(try doFoo()).replaceError()

With trailing syntax for throw chaining could have been more natural:

doFoo().try.replaceError()

and I'd put try after each throwing function (similar to how we can put more than one ! or ?):

_ = foo?.bar?.baz
_ = quz.try.quux.try

ditto for await.

var allIWantForSwiftmas: Is? = nil
12 Likes
  • changing .filter to .filtered (could be backwards-compatible)
  • implicit returns
  • if expressions (could be backwards-compatible)
  • more expression-based syntax instead of statement-based syntax (could be backwards-compatible)
1 Like

Quite a breaking change: denote the minimal integer value (for signed integers, so -128 for Int8, -32768 for Int16, etc), and the maximum integer value (for unsigned integer values) as an overflow marker, that would be propagated across integer ops and end up in the result. As an alternative for signs integers use one-complement representation and denote -0 value for overflow marker. The range of integers will become one value smaller which might be a show stopper for some algorithms but ok for (hopefully) most. overflow == overflow and we can make an arbitrary ruling that overflow > any non overflown int

If that's done we can eventually deprecate trapping on int math overflows, or make it optional so developer can choose between the two behaviours (actually three as there's &+ and co).

I wish we could have fitted in the renaming of dropFirst / dropLast, before the source stability bar was raised. The tense of the verb is wrong, and drop sounds so much more destructive than (for example) removingFirst().

Whilst we maybe technically could still do this change, with a long tail of deprecation, the third-party ecosystem of libraries have also now matched the stdlib names (like in Combine). So it feels somewhat stuck in stone as a quirk of history at this point.

8 Likes

the protocol existential dispatch gets the most blame, but it also has to do with the way UnkeyedDecodingContainer and friends track their codingPath. every decoding operation pushes a debugging breadcrumb onto the stack, which is wasteful and only needed if decoding actually fails.

this is why swift-json opted for a functional (instead of protocol-oriented) decoding interface, which only logs debugging traces if an error is thrown inside the decoder.

1 Like
Minor aside

FWIW, this isn't quite true — codingPath is exposed to also allow types to tell where they are in the encoding tree at a given point, and optionally apply different encoding rules if they need to. It's also helpful for debugging, but that's not the only use-case.

I'm actually surprised that codingPath would be a performance pitfall for the APIs; as far as I remember, nothing in the APIs mandates that codingPath be built eagerly, and it could very well get built lazily upon access.

(I think it's also important to try to separate the implementation of specific encoders and decoders from the API at large, but that's definitely not always easy, given the relatively small number of data points to work with.)

1 Like

I would welcome breaking changes to:

  • the ownership model for improved ergonomics and control over performance
  • Codable, it's really quite cool, but it's time for something faster with fewer limitations
1 Like

I would like to see AnyValue protocol in places where you wouldn't want a class to conform to protocol

3 Likes

I'd like to see compilation conditions back on the drawing board. I really like that they're part of the compilation rather than just string processing, but they're too simple and feel like a forgotten cousin.

Given this thread I'd love to see the related subject of Swift meta language rethought. To that end I'd like to be able to replace gyb (see string processing comment above) with result builder executed at run time to create any Swift eg types.

1 Like

I'd like to see that the enum type has the same features as struct. Currently, enum is incomplete compared with struct.

2 Likes

Adding one more to my list:

  • Removal of callAsFunction in favor of anonymous functions: func (...)

If we then added named subscripts this would align very well:


struct S {
  func name(...) {}
  func (...) {} // instead of `func callAsFunction(...)`
  subscript (...) {}
  subscript name(...) {} // new
}

let s = S()
s.name(...) // calling a named method
s(...) // calling an anonymous method
s[...] // calling a subscript
s.name[...] // calling a named subscript 

And it would signal on why we really need to fix subscript label rules.

10 Likes

I’d be in favor of func _() similar to how we can write func hey(_: Int) {}; hey(_: 0). But all in all, I don’t see why folks are so against callAsFunction. Yes, it’s not the most natural syntax for Swift but it isn’t worth a breaking change in my opinion. Subscript arguments are a blatant exception to Swift’s argument syntax for functions, which are much more commonly declared.

4 Likes

I spoke too soon. Never mind. [Retracted my comment on _().]

Sorry if my post came off as aggressive or overly opinionated. I was honestly asking about the arguments for superseding callAsFunction because I haven’t really followed related discussions.

The thing is it's not syntax at all, it's a special-cased stringly-typed function name that magically gets turned into an anonymous method. It goes against all compile-safe philosophies of Swift, and, although it's a very minor feature, is hands-down one of the most sub-standard evolution acceptances.

1 Like