[Pitch] Elide `some` in Swift 6

Not breaking the ABI matters so little to Swift code that builds everything in to an app bundle. Code distributed as a framework is generally pure Obj-C or they are already doing ABI testing for other reasons. It will force package authors to revisit this which may improve performance of their packages. I wouldn't lose any sleep over it. It could be a performance win for other Swift code and Swift 6 will require packages that want to take advantage of new features to update anyway, so not a big deal to do some minor refactoring. Since it is tied to the language mode, nobody will be forced to adopt it immediately.

+1 on this pitch overall. It seems worth it to me. It forces you to think about boxing/existentials which I think is good. There may be more type information which I think makes it a better default.

2 Likes

This is a very understandable concern but this proposal would not actually get rid of the clarity that comes with deciding between using some and any in your API. I would argue that deciding between some and any is a step beyond the decision to just add protocols to your code. Deciding to introduce a protocol into your code to allow for more abstraction should be considered a separate decision and this proposal would decouple those two decisions.

3 Likes

I see value in the suggestion to replace writing some with writing bare protocols. But I do want to clarify something when you say "where bare protocol doesn't mean specifically some"

If dropping some syntax was a future direction we took, I think it would still make sense to have bare protocols mean some implicitly because the current behavior of bare protocols meaning any can still lead to needing to rewrite code if users run into a limitation with using existentials.

This proposal would re-introduce a third spelling for protocols which is valuable because it helps make it clear across language modes what you mean. Two spellings is a reasonable future direction but I think it should be phased in!

1 Like

I appreciate the thoughtful feedback here! :slight_smile:

Re: should P mean some P?

We've done some migration using this feature and so far our assumption is that most cases where a plain protocol is used, the user actually intended to use some instead of the current default any. I can elaborate more on this point in the proposal. I wouldn't say making bare protocols is really "making the language more opaque" though. Instead this proposal is improving the language design in such a way that enhances user experience; reduces the risk of running into issues that arise when advanced type erasure semantics are present unexpectedly and removing the load to think about such tradeoffs.

We are already encouraging to write some by default because we've deemed any is usually not what we want (which motivated SE-0335 ) For users who are new to generics, this extra step of writing some by default seems less than desirable. Why require users who want to add abstraction to their code using protocols to deep dive into the semantic differences?

Re: Is Swift 6 the right time?

I understand the concern here but this is also very similar to the type of code that will break with implicit open existentials in Swift 6, SE-0352, so this proposal is in keeping with the direction we are heading in.

In the case where you leave your code as is, we are planning to include fixits to add any for the function reference case and other edge cases we are surfacing through migration to help cut down on the need to manually track down such cases. But having an option to migrate all uses of P to any P is an interesting idea that I can consider. I can definitely add more depth to the proposal to discuss a migration plan and options!

5 Likes

My current opinion/gut feeling is against this pitch of allowing the elision of some. I worry that blurring the line between types and type-constraints (protocols) is not a good thing, and that this pitch would rob us of the opportunity to escape from that issue in Swift 6.

I think that what could convince me that this is a good idea would be a thorough conceptual/philosophical breakdown of types vs protocols from someone with more well-rounded knowledge than I, in which the thesis is defended that preserving the distinction in the discussed syntax is more noise than signal.

If I truly believed in the change then getting to drop some from a visual standpoint would be rather nice…

6 Likes

I think eliding some will damage consistency with classes in syntax.

class A {}
class B1: A {}
class B2: A {}
// possible
func foo() -> A {
    if condition {
        return B1()
    } else {
        return B2()
    }
}

protocol P {}
struct S1: P {}
struct S2: P {}

// impossible
func foo() -> P {
    if condition {
        return S1()
    } else {
        return S2()
    }
}
2 Likes

It's a good and worthy proposal to debate but I'd vote no.

In an alternate universe where we hadn't already decided a long time ago to make both some and any explicit in order to make it clear when a protocol is being used; to make this a breaking change in Swift 6; and in the meantime to notify everyone via compiler warnings to start adding "any" everywhere... in such a universe I might feel differently about your proposal.

But even then, I'd be concerned with:

  • @objc protocol cannot have an opaque type
  • a variable named var bar: AnyFooable (where AnyFooable is a protocol) should not be implicitly an opaque type
  • it could still be source-breaking if someone currently has code calling a func foo(_ x: Fooble) with a variable var x: any Fooable, since under your proposal now the compiler requires you call it with an opaque type and won't allow it to be called with an existential
  • this could cause lots of bugs because changing the types of variables in a way that can change their dispatch to static instead of dynamic and memory from heap to stack could have lots of unforeseen consequences at runtime or in tests
  • opaque types don't let you see the type metadata in debugging (opaque means you cannot introspect them), which is not a kind of change I'd want implicitly happening

So I have to agree with @mtsrodrigues above... the ship seems like it sailed regarding which direction Swift is going with some and any. The debates were had. The decisions were accepted. The compiler warnings were added. The blog posts went up. Nothing is set in stone and you're welcome to propose any change, but I just think it's too late.

While I appreciate your argument about less "cognitive load" for beginners (I was against making any explicit at first also), actually, adding cognitive load/friction was the point of not having an implicit default anymore. We want people to know when they are using a protocol.

The rest of your pitch seems to be an argument of why opaque types and generics are "better," but I think it's not quite accurate; they're just different, with pros and cons either way.

This doesn't seem right by my understanding of Swift. Initializer requirements on an any protocol-type won't be available on the protocol-type, but other static requirements can be. Also, a value of type any Foo is guaranteed to have an implementation of the protocol's non-static requirements. Further, implementations defined in a protocol or conformance extension (static or otherwise) work the same way for both opaques and existentials.

The reason we're making any explicit is not because "existentials are bad" but because implicit behaviors are bad and over-use of abstractions is bad. It should be clear whenever you're using an abstraction as opposed to a concrete type.

So the decision was made that when using a protocol, opaquely or existentially, it ought to never happen implicitly.

As to the concern of some cluttering up function declarations, consider using a generic type constrained by a protocol instead. That way, you don't have any or some and most reviewers accept a single character as the type :smiley:

Just my take.

9 Likes

I think this change would make Swift more complicated and harder to learn in exchange for a fairly minor benefit.

If the proposal was to remove some entirely I would find it much more compelling. I'm unclear on if this pitch allows eliminating only some uses of some or all of them. If it's all of them then it's just a redundant keyword that exists only for the sake of supporting building as either Swift 5 or 6, which isn't the biggest deal but is still something which will trip up beginners. If it's only some of them then this is introducing something very confusing.

The bigger problem is that it makes all currently existing documentation, blog posts, SO answers, tutorials, etc. about the current version of Swift incorrect in a way that most readers will not notice. When a code snippet in a blog post fails to compile it'll frustrate the reader, but it's at least a strong hint that the thing they're reading is out of date or incorrect. If it works fine though, the reader is probably going to assume that the description of what it's doing is accurate as well, and so learn actively incorrect things. The fact that many uses of existentials work fine as some is somewhat a downside from the perspective of teaching new users the language.

It'd be one thing if this was reusing syntax from Swift 1.0, but proposing to repurpose syntax which the most recent version of the languge accepts without warnings is extremely over-eager.

9 Likes

+1. I think improving the language is worth the inconveniences it may cause. The experienced developers already face far worse issues they need to handle on a daily basis. Do any of you have to go back and forth with a mix of languages like JavaScript, C++, Java and Swift regularly? How does that compare to this one time shift?

Well sure, that's the idea isn't it? What I meant was that either we get rid of some - and then eventually people will not think of it as "implicit some", or we keep it, but if we keep it I really don't see any advantage at all in being able to sometimes elide it. On the contrary, the effect will be more confusion, when there are three ways to spell two different concept. Especially when one of the ways used to mean something else.

This is the exactly the opposite of what it will do. Clearly it will increase confusion when the meaning of a syntax suddenly changes - especially across language modes - how could it possibly make things clearer?

The only argument for this change that I can discern in this pitch and elsewhere is that it makes it possible for old code that used implicit any in the wrong way, will continue to work without change, and mean a different thing. This is dubious in so many ways. First of all I don't think migration should be an important factor at all, we will have to live with the syntax for years, maybe decades. Secondly, the cases where this migration is possible must be rather few. Thirdly, is improper use of (implicit) any really a major source of performance problems? Or a source at all?

It seems like a very niche reason to make the language significantly less clear.

I understand that it is possible to do this, but what is an argument for why it would be an improvement?

7 Likes

If I'm understanding correctly, I think that this thing you're referring to:

is a step in the direction of arguing against this thesis I mentioned:

I'm interested to know more about the possibly subtle differences at play here, because that would help me judge whether I think that I personally would prefer the syntax to preserve or elide the fact that a parameter is generic.

Let's say I'm faced with modifying this function:

func sendPushNotification (_ pushNotification: PushNotification) async throws {
    // code...
}

Does anybody feel able to list out all of the ways in which it would make a difference to me-the-function-author that PushNotification is a protocol and not a struct/enum, and therefore that pushNotification really has type some PushNotification?

1 Like

You can do this in Rust and it is used pretty regularly, so it cannot be too bad in terms of performance. I've always wondered why the Swift type checker still has performance problems even though it is mostly restricted to type-checking a single expression at a time while Rust does much heavier type-checking very seamlessly.

1 Like

Thanks for your response @angela-laar!

Yeah I it would be great for the proposal to include discussion of any empirical data that has already been gathered, and to be as concrete as possible. IMO "most cases" still sounds pretty hedge-y to me.

My use of "more opaque" should be construed fairly narrowly. I am specifically talking about the issue that others in the thread have pointed out where, from looking at declarations such as:

func f(_: J)
var j: J

it will become impossible to infer from the signatures alone some potentially salient details about how these declarations can be used, even if I am fully aware of the difference between any and some. Instead, I must either click through to the definition of each type to determine if it's a protocol, or attempt to use the declarations in an invalid way to discover the problem. That is, to me, a strictly worse user experience than the status quo.

Now, @jeremyabannister's point is also relevant here:

I will admit that I cannot off the top of my head rattle off all the ways in which using some P instead of any P might change or affect the use of a declaration. So, perhaps we think that the potential issues are narrow enough that the benefit of not requiring users to insert 'some' outweighs the costs. But in general I bias towards favoring ease of reading over ease of writing.

I disagree that "apply fix-it" requires a deep dive. IMO an error along the lines of "bare protocol name cannot be used as a type" along with a "fix it: add 'some'" is reasonably in line with progressive disclosure. It would enforce the idea that some should be the default and prevent users who don't care about the difference from having to think too hard about it until they need to.

Sure, I concede that such changes are probably unlikely to cause any major issues, I only mean to highlight that I don't the current text paints a full picture of the source compatibility story. I also think the problem is somewhat different in kind to that noted in SE-0352, where we are bringing new possible overloads in-bounds—here, we're taking two overloads which both already type check and changing their ranking.

Yeah I expect we can catch the most obvious cases but I am still moderately worried that every additional edge case we have to account for increases the complexity and makes it more likely that something will slip through the cracks. But if we have reason to believe that we really can migrate large complex codebases with a 'one-click' solution (as the 'add any' migration would be) in the majority of cases, I would love to be contradicted. :slightly_smiling_face:

I will also note that even if we can get to a 'one-click' migration with the change pitched here, the difficulty for users who wish to ensure behavior won't change with the migration is still quite high (since the migration will be prioritizing a smaller diff over fewer semantic changes). So I still think the option to add any everywhere would be a good one.

Additionally, IMO the proposal should more fully grapple with the story for all library authors, including those that are not writing resilient libraries as @xwu calls out above ("all public APIs"). We can't count on these authors to be running any kind of post-migration check on their code besides "does it still compile," and they may not have test cases that will catch the situations where the existential-to-generic conversion will break their clients.

Yeah as mentioned above I think having a more complete migration story would greatly help the proposal!

2 Likes

I am very, very supportive of the goals of this pitch. Any attempt to mitigate the effects of SE-0335 as passed is welcome in my book.

I had a quick look at last night’s developer snapshot trying to compile GitHub - Alamofire/Alamofire: Elegant HTTP Networking in Swift. I know this is a work in progress but perhaps we’re going to have to refine the idea and compromise a little on migrating to some. The list of errors trying to compile a fresh clone and using swift build -Xswiftc -enable-experimental-feature -Xswiftc ImplicitSome is below.

https://johnholdsworth.com/firstattempt.txt

The first impression these errors give is perhaps we can elide to some except in the following cases:

  • Where the bare protocol is in a Container (Array, Dictionary or Optional)
  • Where the bare protocol is the type of an uninitialised property.
  • Where the bare protocol specifies the argument to a closure.
  • Where eliding to any satisfied a legacy protocol constraint.

I’ve edited the project manually as if these rules were applied and it builds until there is a crash later in code generation which may or may not be relevant.

Suggested tempering of elision to `some`
diff --git a/Source/AFError.swift b/Source/AFError.swift
index 8cd60c7..37c568c 100644
--- a/Source/AFError.swift
+++ b/Source/AFError.swift
@@ -363,7 +363,7 @@ extension AFError {
 
 extension AFError {
     /// The `URLConvertible` associated with the error.
-    public var urlConvertible: URLConvertible? {
+    public var urlConvertible: (any URLConvertible)? {
         guard case let .invalidURL(url) = self else { return nil }
         return url
     }
@@ -377,7 +377,7 @@ extension AFError {
     /// The underlying `Error` responsible for generating the failure associated with `.sessionInvalidated`,
     /// `.parameterEncodingFailed`, `.parameterEncoderFailed`, `.multipartEncodingFailed`, `.requestAdaptationFailed`,
     /// `.responseSerializationFailed`, `.requestRetryFailed` errors.
-    public var underlyingError: Error? {
+    public var underlyingError: (any Error)? {
         switch self {
         case let .multipartEncodingFailed(reason):
             return reason.underlyingError
@@ -460,7 +460,7 @@ extension AFError {
 }
 
 extension AFError.ParameterEncodingFailureReason {
-    var underlyingError: Error? {
+    var underlyingError: (any Error)? {
         switch self {
         case let .jsonEncodingFailed(error),
              let .customEncodingFailed(error):
@@ -472,7 +472,7 @@ extension AFError.ParameterEncodingFailureReason {
 }
 
 extension AFError.ParameterEncoderFailureReason {
-    var underlyingError: Error? {
+    var underlyingError: (any Error)? {
         switch self {
         case let .encoderFailed(error):
             return error
@@ -503,7 +503,7 @@ extension AFError.MultipartEncodingFailureReason {
         }
     }
 
-    var underlyingError: Error? {
+    var underlyingError: (any Error)? {
         switch self {
         case let .bodyPartFileNotReachableWithError(_, error),
              let .bodyPartFileSizeQueryFailedWithError(_, error),
@@ -564,7 +564,7 @@ extension AFError.ResponseValidationFailureReason {
         }
     }
 
-    var underlyingError: Error? {
+    var underlyingError: (any Error)? {
         switch self {
         case let .customValidationFailed(error):
             return error
@@ -594,7 +594,7 @@ extension AFError.ResponseSerializationFailureReason {
         }
     }
 
-    var underlyingError: Error? {
+    var underlyingError: (any Error)? {
         switch self {
         case let .jsonSerializationFailed(error),
              let .decodingFailed(error),
@@ -632,7 +632,7 @@ extension AFError.ServerTrustFailureReason {
         }
     }
 
-    var underlyingError: Error? {
+    var underlyingError: (any Error)? {
         switch self {
         case let .customEvaluationFailed(error):
             return error
diff --git a/Source/AuthenticationInterceptor.swift b/Source/AuthenticationInterceptor.swift
index c3a3f31..625cec1 100644
--- a/Source/AuthenticationInterceptor.swift
+++ b/Source/AuthenticationInterceptor.swift
@@ -81,7 +81,7 @@ public protocol Authenticator: AnyObject {
     ///   - credential: The `Credential` to refresh.
     ///   - session:    The `Session` requiring the refresh.
     ///   - completion: The closure to be executed once the refresh is complete.
-    func refresh(_ credential: Credential, for session: Session, completion: @escaping (Result<Credential, Error>) -> Void)
+    func refresh(_ credential: Credential, for session: Session, completion: @escaping (Result<Credential, any Error>) -> Void)
 
     /// Determines whether the `URLRequest` failed due to an authentication error based on the `HTTPURLResponse`.
     ///
@@ -193,7 +193,7 @@ public class AuthenticationInterceptor<AuthenticatorType>: RequestInterceptor wh
     private struct AdaptOperation {
         let urlRequest: URLRequest
         let session: Session
-        let completion: (Result<URLRequest, Error>) -> Void
+        let completion: (Result<URLRequest, any Error>) -> Void
     }
 
     private enum AdaptResult {
@@ -247,7 +247,7 @@ public class AuthenticationInterceptor<AuthenticatorType>: RequestInterceptor wh
 
     // MARK: Adapt
 
-    public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
+    public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, any Error>) -> Void) {
         let adaptResult: AdaptResult = $mutableState.write { mutableState in
             // Queue the adapt operation if a refresh is already in place.
             guard !mutableState.isRefreshing else {
diff --git a/Source/EventMonitor.swift b/Source/EventMonitor.swift
index 3b09671..4333f0c 100644
--- a/Source/EventMonitor.swift
+++ b/Source/EventMonitor.swift
@@ -312,13 +312,13 @@ extension EventMonitor {
 public final class CompositeEventMonitor: EventMonitor {
     public let queue = DispatchQueue(label: "org.alamofire.compositeEventMonitor", qos: .utility)
 
-    let monitors: [EventMonitor]
+    let monitors: [any EventMonitor]
 
-    init(monitors: [EventMonitor]) {
+    init(monitors: [any EventMonitor]) {
         self.monitors = monitors
     }
 
-    func performEvent(_ event: @escaping (EventMonitor) -> Void) {
+    func performEvent(_ event: @escaping (any EventMonitor) -> Void) {
         queue.async {
             for monitor in self.monitors {
                 monitor.queue.async { event(monitor) }
@@ -571,7 +571,7 @@ public final class CompositeEventMonitor: EventMonitor {
 /// `EventMonitor` that allows optional closures to be set to receive events.
 open class ClosureEventMonitor: EventMonitor {
     /// Closure called on the `urlSession(_:didBecomeInvalidWithError:)` event.
-    open var sessionDidBecomeInvalidWithError: ((URLSession, Error?) -> Void)?
+    open var sessionDidBecomeInvalidWithError: ((URLSession, (any Error)?) -> Void)?
 
     /// Closure called on the `urlSession(_:task:didReceive:completionHandler:)`.
     open var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> Void)?
@@ -589,7 +589,7 @@ open class ClosureEventMonitor: EventMonitor {
     open var taskDidFinishCollectingMetrics: ((URLSession, URLSessionTask, URLSessionTaskMetrics) -> Void)?
 
     /// Closure called on the `urlSession(_:task:didCompleteWithError:)` event.
-    open var taskDidComplete: ((URLSession, URLSessionTask, Error?) -> Void)?
+    open var taskDidComplete: ((URLSession, URLSessionTask, (any Error)?) -> Void)?
 
     /// Closure called on the `urlSession(_:taskIsWaitingForConnectivity:)` event.
     open var taskIsWaitingForConnectivity: ((URLSession, URLSessionTask) -> Void)?
diff --git a/Source/MultipartUpload.swift b/Source/MultipartUpload.swift
index ceda21f..04df61a 100644
--- a/Source/MultipartUpload.swift
+++ b/Source/MultipartUpload.swift
@@ -31,7 +31,7 @@ final class MultipartUpload {
     @Protected
     private(set) var multipartFormData: MultipartFormData
     let encodingMemoryThreshold: UInt64
-    let request: URLRequestConvertible
+    let request: any URLRequestConvertible
     let fileManager: FileManager
 
     init(encodingMemoryThreshold: UInt64,
diff --git a/Source/Request.swift b/Source/Request.swift
index 3d64743..148e98b 100644
--- a/Source/Request.swift
+++ b/Source/Request.swift
@@ -72,11 +72,11 @@ public class Request {
     /// The queue used for all serialization actions. By default it's a serial queue that targets `underlyingQueue`.
     public let serializationQueue: DispatchQueue
     /// `EventMonitor` used for event callbacks.
-    public let eventMonitor: EventMonitor?
+    public let eventMonitor: (any EventMonitor)?
     /// The `Request`'s interceptor.
-    public let interceptor: RequestInterceptor?
+    public let interceptor: (any RequestInterceptor)?
     /// The `Request`'s delegate.
-    public private(set) weak var delegate: RequestDelegate?
+    public private(set) weak var delegate: (any RequestDelegate)?
 
     // MARK: - Mutable State
 
@@ -89,9 +89,9 @@ public class Request {
         /// `ProgressHandler` and `DispatchQueue` provided for download progress callbacks.
         var downloadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)?
         /// `RedirectHandler` provided for to handle request redirection.
-        var redirectHandler: RedirectHandler?
+        var redirectHandler: (any RedirectHandler)?
         /// `CachedResponseHandler` provided to handle response caching.
-        var cachedResponseHandler: CachedResponseHandler?
+        var cachedResponseHandler: (any CachedResponseHandler)?
         /// Queue and closure called when the `Request` is able to create a cURL description of itself.
         var cURLHandler: (queue: DispatchQueue, handler: (String) -> Void)?
         /// Queue and closure called when the `Request` creates a `URLRequest`.
@@ -165,7 +165,7 @@ public class Request {
     // MARK: Redirect Handling
 
     /// `RedirectHandler` set on the instance.
-    public private(set) var redirectHandler: RedirectHandler? {
+    public private(set) var redirectHandler: (any RedirectHandler)? {
         get { $mutableState.redirectHandler }
         set { $mutableState.redirectHandler = newValue }
     }
@@ -173,7 +173,7 @@ public class Request {
     // MARK: Cached Response Handling
 
     /// `CachedResponseHandler` set on the instance.
-    public private(set) var cachedResponseHandler: CachedResponseHandler? {
+    public private(set) var cachedResponseHandler: (any CachedResponseHandler)? {
         get { $mutableState.cachedResponseHandler }
         set { $mutableState.cachedResponseHandler = newValue }
     }
@@ -1083,7 +1083,7 @@ public protocol RequestDelegate: AnyObject {
 /// `Request` subclass which handles in-memory `Data` download using `URLSessionDataTask`.
 public class DataRequest: Request {
     /// `URLRequestConvertible` value used to create `URLRequest`s for this instance.
-    public let convertible: URLRequestConvertible
+    public let convertible: any URLRequestConvertible
     /// `Data` read from the server so far.
     public var data: Data? { mutableData }
 
@@ -1244,7 +1244,7 @@ public final class DataStreamRequest: Request {
     }
 
     /// `URLRequestConvertible` value used to create `URLRequest`s for this instance.
-    public let convertible: URLRequestConvertible
+    public let convertible: any URLRequestConvertible
     /// Whether or not the instance will be cancelled if stream parsing encounters an error.
     public let automaticallyCancelOnStreamError: Bool
 
@@ -1778,7 +1778,7 @@ public class UploadRequest: DataRequest {
     // MARK: Initial State
 
     /// The `UploadableConvertible` value used to produce the `Uploadable` value for this instance.
-    public let upload: UploadableConvertible
+    public let upload: any UploadableConvertible
 
     /// `FileManager` used to perform cleanup tasks, including the removal of multipart form encoded payloads written
     /// to disk.
diff --git a/Source/RequestInterceptor.swift b/Source/RequestInterceptor.swift
index 7ed39a5..f591ee7 100644
--- a/Source/RequestInterceptor.swift
+++ b/Source/RequestInterceptor.swift
@@ -43,7 +43,7 @@ public protocol RequestAdapter {
     ///   - urlRequest: The `URLRequest` to adapt.
     ///   - session:    The `Session` that will execute the `URLRequest`.
     ///   - completion: The completion handler that must be called when adaptation is complete.
-    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void)
+    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, any Error>) -> Void)
 
     /// Inspects and adapts the specified `URLRequest` in some manner and calls the completion handler with the Result.
     ///
@@ -51,11 +51,11 @@ public protocol RequestAdapter {
     ///   - urlRequest: The `URLRequest` to adapt.
     ///   - state:      The `RequestAdapterState` associated with the `URLRequest`.
     ///   - completion: The completion handler that must be called when adaptation is complete.
-    func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result<URLRequest, Error>) -> Void)
+    func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result<URLRequest, any Error>) -> Void)
 }
 
 extension RequestAdapter {
-    public func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result<URLRequest, Error>) -> Void) {
+    public func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result<URLRequest, any Error>) -> Void) {
         adapt(urlRequest, for: state.session, completion: completion)
     }
 }
@@ -89,7 +89,7 @@ extension RetryResult {
         }
     }
 
-    var error: Error? {
+    var error: (any Error)? {
         guard case let .doNotRetryWithError(error) = self else { return nil }
         return error
     }
@@ -118,7 +118,7 @@ public protocol RequestRetrier {
 public protocol RequestInterceptor: RequestAdapter, RequestRetrier {}
 
 extension RequestInterceptor {
-    public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
+    public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, any Error>) -> Void) {
         completion(.success(urlRequest))
     }
 
@@ -148,11 +148,11 @@ open class Adapter: RequestInterceptor {
         self.adaptHandler = adaptHandler
     }
 
-    open func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
+    open func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, any Error>) -> Void) {
         adaptHandler(urlRequest, session, completion)
     }
 
-    open func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result<URLRequest, Error>) -> Void) {
+    open func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result<URLRequest, any Error>) -> Void) {
         adaptHandler(urlRequest, state.session, completion)
     }
 }
@@ -207,9 +207,9 @@ extension RequestRetrier where Self == Retrier {
 /// `RequestInterceptor` which can use multiple `RequestAdapter` and `RequestRetrier` values.
 open class Interceptor: RequestInterceptor {
     /// All `RequestAdapter`s associated with the instance. These adapters will be run until one fails.
-    public let adapters: [RequestAdapter]
+    public let adapters: [any RequestAdapter]
     /// All `RequestRetrier`s associated with the instance. These retriers will be run one at a time until one triggers retry.
-    public let retriers: [RequestRetrier]
+    public let retriers: [any RequestRetrier]
 
     /// Creates an instance from `AdaptHandler` and `RetryHandler` closures.
     ///
@@ -242,14 +242,14 @@ open class Interceptor: RequestInterceptor {
         self.retriers = retriers + interceptors
     }
 
-    open func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
+    open func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, any Error>) -> Void) {
         adapt(urlRequest, for: session, using: adapters, completion: completion)
     }
 
     private func adapt(_ urlRequest: URLRequest,
                        for session: Session,
-                       using adapters: [RequestAdapter],
-                       completion: @escaping (Result<URLRequest, Error>) -> Void) {
+                       using adapters: [any RequestAdapter],
+                       completion: @escaping (Result<URLRequest, any Error>) -> Void) {
         var pendingAdapters = adapters
 
         guard !pendingAdapters.isEmpty else { completion(.success(urlRequest)); return }
@@ -266,14 +266,14 @@ open class Interceptor: RequestInterceptor {
         }
     }
 
-    open func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result<URLRequest, Error>) -> Void) {
+    open func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result<URLRequest, any Error>) -> Void) {
         adapt(urlRequest, using: state, adapters: adapters, completion: completion)
     }
 
     private func adapt(_ urlRequest: URLRequest,
                        using state: RequestAdapterState,
-                       adapters: [RequestAdapter],
-                       completion: @escaping (Result<URLRequest, Error>) -> Void) {
+                       adapters: [any RequestAdapter],
+                       completion: @escaping (Result<URLRequest, any Error>) -> Void) {
         var pendingAdapters = adapters
 
         guard !pendingAdapters.isEmpty else { completion(.success(urlRequest)); return }
@@ -300,7 +300,7 @@ open class Interceptor: RequestInterceptor {
     private func retry(_ request: Request,
                        for session: Session,
                        dueTo error: Error,
-                       using retriers: [RequestRetrier],
+                       using retriers: [any RequestRetrier],
                        completion: @escaping (RetryResult) -> Void) {
         var pendingRetriers = retriers
 
diff --git a/Source/ResponseSerialization.swift b/Source/ResponseSerialization.swift
index 3097364..03130ef 100644
--- a/Source/ResponseSerialization.swift
+++ b/Source/ResponseSerialization.swift
@@ -65,7 +65,7 @@ public protocol DownloadResponseSerializerProtocol {
 /// A serializer that can handle both data and download responses.
 public protocol ResponseSerializer: DataResponseSerializerProtocol & DownloadResponseSerializerProtocol {
     /// `DataPreprocessor` used to prepare incoming `Data` for serialization.
-    var dataPreprocessor: DataPreprocessor { get }
+    var dataPreprocessor: any DataPreprocessor { get }
     /// `HTTPMethod`s for which empty response bodies are considered appropriate.
     var emptyRequestMethods: Set<HTTPMethod> { get }
     /// HTTP response codes for which empty response bodies are considered appropriate.
diff --git a/Source/Result+Alamofire.swift b/Source/Result+Alamofire.swift
index 39ac286..6fe83ab 100644
--- a/Source/Result+Alamofire.swift
+++ b/Source/Result+Alamofire.swift
@@ -79,7 +79,7 @@ extension Result {
     ///
     /// - returns: A `Result` containing the result of the given closure. If this instance is a failure, returns the
     ///            same failure.
-    func tryMap<NewSuccess>(_ transform: (Success) throws -> NewSuccess) -> Result<NewSuccess, Error> {
+    func tryMap<NewSuccess>(_ transform: (Success) throws -> NewSuccess) -> Result<NewSuccess, any Error> {
         switch self {
         case let .success(value):
             do {
@@ -105,7 +105,7 @@ extension Result {
     ///
     /// - Returns: A `Result` instance containing the result of the transform. If this instance is a success, returns
     ///            the same success.
-    func tryMapError<NewFailure: Error>(_ transform: (Failure) throws -> NewFailure) -> Result<Success, Error> {
+    func tryMapError<NewFailure: Error>(_ transform: (Failure) throws -> NewFailure) -> Result<Success, any Error> {
         switch self {
         case let .failure(error):
             do {
diff --git a/Source/Session.swift b/Source/Session.swift
index 4232f85..3be3c97 100644
--- a/Source/Session.swift
+++ b/Source/Session.swift
@@ -54,17 +54,17 @@ open class Session {
     public let serializationQueue: DispatchQueue
     /// `RequestInterceptor` used for all `Request` created by the instance. `RequestInterceptor`s can also be set on a
     /// per-`Request` basis, in which case the `Request`'s interceptor takes precedence over this value.
-    public let interceptor: RequestInterceptor?
+    public let interceptor: (any RequestInterceptor)?
     /// `ServerTrustManager` instance used to evaluate all trust challenges and provide certificate and key pinning.
     public let serverTrustManager: ServerTrustManager?
     /// `RedirectHandler` instance used to provide customization for request redirection.
-    public let redirectHandler: RedirectHandler?
+    public let redirectHandler: (any RedirectHandler)?
     /// `CachedResponseHandler` instance used to provide customization of cached response handling.
-    public let cachedResponseHandler: CachedResponseHandler?
+    public let cachedResponseHandler: (any CachedResponseHandler)?
     /// `CompositeEventMonitor` used to compose Alamofire's `defaultEventMonitors` and any passed `EventMonitor`s.
     public let eventMonitor: CompositeEventMonitor
     /// `EventMonitor`s included in all instances. `[AlamofireNotifications()]` by default.
-    public let defaultEventMonitors: [EventMonitor] = [AlamofireNotifications()]
+    public let defaultEventMonitors: [any EventMonitor] = [AlamofireNotifications()]
 
     /// Internal map between `Request`s and any `URLSessionTasks` that may be in flight for them.
     var requestTaskMap = RequestTaskMap()

I’m sure this sounds "not serious" and a bit arbitrary, being neither eliding to one or the other in a seemingly non-deterministic way but the rules aren't that complicated and essentially amount to continuing to use an existential when the value is contained in another type so as not to leak it’s genericisity to the containing type.

In this way the net effect of this aspect of Swift 6 would be reaping the benefits of a partial migration directly to some without trying to apply it where it wouldn't have been appropriate anyway which seems to be reasonably easy to determine.

Alamofire is great starting point. It is a popular, rich codebase which makes extensive use of protocols. If it can be made to build without modification that would bode well for the existing Swift codebase.

4 Likes

With respect, I must dissent from this proposal. I believe it represents a significant decrease in readability and comprehensibility of Swift code for very little benefit.

I belive much of the crux of the argument for this pitch (aside from the migration benefit, addressed below, and saving five characters of typing) is expressed in the following quote:

Swift 6 could apply this same principle [concision as a form of progressive disclosure] to plain protocol names in other contexts. Such a practice could be invaluable for beginners learning Swift, removing the mental load of comparing tradeoffs between some and any when adding protocols to their code

However, most people who are learning a new language do so by rote at first and then later learn why they were taught what they were taught. Telling people who are new to using protocols that "you write protocols as some P when you use them in functions or variables" is close enough to true for almost any student and requires no cognitive overhead at all besides remembering a syntax, which is par for the course when learning a language. I actually think the distinctive syntax for protocols is an advantage, as it helps make clear to learners that protocols are a different kind of thing than enums, structs, classes and (for the very brave) actors. Perhaps that will pique their interest to inquire about why and how they're different.

It is, of course, true to say that a developer who's familiar with Swift (in a future where this pitch is accepted) would know that a bare P is shorthand for generic P. It's not true, however, that a developer reading some code would know that P is a protocol name. Indeed, this proposal would make it impossible to know that (in most cases) without going to the declaration of P. Progressive disclosure is a wonderful design tenet, but I think it shouldn't be used to justify adding a papercut to experienced devs' lives.

I'm sympathetic to the idea of easing the migration to Swift 6, but this strikes me as paying a permanent price (worse syntax, as argued above) for a one-time problem. And, at that, a one-time problem that occurs at a time of a user's choosing and that can be resolved automatically with guaranteed correctness. To me (someone who works daily in a very large Swift codebase with a dozen or so other developers), this argument holds no sway in light of the cost.

I'm also concerned about the subtle breaking of expected behavior (any -> some will sometimes result in unexpected behavior changes), but as that, too, is a one-time thing in a major language revision, I feel that sort of change would be acceptable if the benefits outweighed the other costs.

I thank you for your consideration and for all the hard work you and everyone else have put into making Swift such a great language.

30 Likes

To clarify, while this list of differences that you mention is of course also highly relevant to this discussion, I was asking about the list of differences between the ways I can use the value pushNotification in this function, where PushNotification is a struct (or enum, etc.):

func sendPushNotification (_ pushNotification: PushNotification) async throws {
    // code...
}

vs. this function where PushNotification is a protocol.

func sendPushNotification (_ pushNotification: some PushNotification) async throws {
    // code...
}

because if this proposal were approved then they would both look like the first function, but would continue to behave differently (assuming the list of differences I'm asking for is not empty).

P.S. Thank you, whoever you were, for dark mode on the Swift forums! In the middle of writing this post my MacBook switched to dark mode and the Swift forums did too along with it, which is a hugely appreciated recent upgrade. Thank you!
4 Likes

I have been looking for surprising side effects in the interaction of this pitch with the fact that overload resolution favors existentials.

First, a reminder of what I mean by "overload resolution favors existentials". It is this:

protocol P { }
struct S: P { }
func foo(_ x: any P) { print("P existential") }
func foo(_ x: some P) { print("P generic") }
// Prints "P existential"
foo(S())

Yes, that's how the language works today - one of its little gotchas. It's hard to consider the existential version as "more specific", and it would be interesting to know why avoiding the generic and specializable method was considered as the best implementation strategy. Anyway, that's not the point of this post. Overload resolution favors existentials, that's what's important.

So I found this. The code below changes its behavior depending on how the bare P is interpreted:

protocol P { }
protocol Q { }
struct S: P, Q { }
func foo(_ x: P) { print("Bare P") }
func foo(_ x: some P & Q) { print("P & Q generic") }
// Prints "Bare P" if P is interpreted as `any P`
// Prints "P & Q generic" if P is interpreted as `some P`
foo(S())

I have difficulties deciding if this would create a lot of churn. But this definitely is a breaking change that is worth mentioning in this pitch.

9 Likes

It's certainly worth mentioning in the proposal. My feeling is this change is very much for the better (and any semantic change resulting could as or more likely fix bugs as cause them). It's undesirable for P to win out over P & Q, the latter being more specific. The fact that existentials are ranked higher than generics is a strange counterintuitive quirk that (this needs a separate thread for discussion) should also be fixed in Swift 6.

8 Likes

Why is it for the better though? Apart from the migration argument (which I disagree with), how would this actually make Swift better? It seems significantly more confusing to me.

A hand-wavy description of Swift's overload resolution is that it picks "the most specific" function. So it'll prefer e.g. a method on the concrete String to a generic method on StringProtocol – the concrete one has more information about the type so is more likely to be optimal. It'll also prefer a method on RandomAccessCollection over one on Collection – the random access one can take advantage of guaranteed constant-time movement around the collection.

It also prefers functions that don't need implicit conversion so for example, a function that takes a String? is preferred over one that takes String. This goes for conversion to Any too. A function f<T>(_ t: T) is preferred over one that takes f(_ a: Any).

There are patches where this intuition breaks down though. Let's take a simpler example, that works today without upcoming or experimental features:

func f(_ a: any Collection) {
  print("A collection of mystery")
}

func f(_ a: some Collection<Int>) {
  print("A collection of sum ", a.reduce(0,+))
}

f([1,2,3]) // prints A collection of mystery

This is clearly not the best choice. Of course a function that knows the collection contains an Int should be picked over one that knows nothing about the contents of the collection and can't be optimized for it (perhaps it attempts to figure out what the collection contains using as? at runtime... which is very inefficient). But it works this way because of the path Swift has taken over the years, and overload resolution not being updated to keep up.

@gwendal.roue's example is similar. Of course overload resolution should pick a function that knows its argument conforms to P & Q over a function that knows the type conforms only to P.

Now as it happens, this is happening as a byproduct of P moving from being an existential to a generic argument. Another way of achieving this would be to change the overload rules to prefer generic arguments with the same constraints as an existential. But in either case, we are moving more towards the "correct" (IMO) behavior. I think we should do both, but this thread is about a pitch to do the former.

5 Likes