Realistic usecases of the new Swift 5.7 protocol abilities

In Swift 5.7 we have new features for protocols.
https://github.com/apple/swift-evolution/blob/main/proposals/0309-unlock-existential-types-for-all-protocols.md
https://github.com/apple/swift-evolution/blob/main/proposals/0352-implicit-open-existentials.md
But as I think about it, I couldn't find any place where this would be needed and would not contradict any basic architectural principles.
I've found an article How to Achieve Dynamic Dispatch Using Generic Protocols in Swift 5.7 - Swift Senpai explaining this. Generally, the example shown there suggests that you can use the new features

  • to call the functions that do not have associatedtype of the protocol in their declaration.
  • if the associatedtype is the return type of the function, the only way to use it apart from just as 'usual' any type is to pass it to other function of the type conforming to the protocol.
  • if the associatedtype is a type of an argument of the function, the only way to actually call the function is to have a method in the protocol that is returning the value of associatedtype (either as a function in a protocol itself or indirectly in the protocol / base class of the associatedtype itself or if we have some relations that bind the associatedtype to some other type, for example, through where clause).

So, the first case could be achieved today easily just by separating protocol with no associatedtype requirements from the protocol with it and storing the values of its type. The second and third use cases create an obligation to use two protocol methods for each other, which is questionable from my perspective. The third use case also is questionable because we generally create a requirement for the type to declare certain initializer, when it is strongly recommended not to do that, as init is a main place to pass some unique dependencies.

Even if applicable, second and third usecases are not applicable for the vast majority of existing protocols with associatedtype in the standard library. It is senseless to create any Equatable, any Collection or any Hashable as there's simply no way to use them without specifically knowing their associatedtype. Maybe in some really special cases like calling Collection.count from any Collection (why not to store the count itself and not any Collection in this case?)

Of course, you can dynamically downcast the values of any Protocol to some special constrained protocols (from iOS 16), but is the downcast the best practice that needs to be encouraged?

So I wonder if you've found any useful usecases for this feature, some situations where its absence is really making code more complicated and less verifiable at this point.

There are a couple of new things in SwiftUI that use these new features (eg making NavigationPath conform to Codable) which are pretty cool.

Personally they’ve helped remove a lot of the type erasure I had in my Swift projects (eg I’ve been able to remove all uses of AnyPublisher and AnyRandomAccessCollection) which is nice because such types can be difficult to make yourself and usually have some kind of “can be bad for performance” warning.

I also disagree it your point about separating protocols. In my experience at least, it’s much better to have a single protocol. It’s annoying to have two that represent the same thing but have to be treated differently (eg nothing should conform to the one without associated types by itself).

Here's an example of how I'm using the new abilities. It demonstrates being able to store a heterogeneous array all conforming to a particular protocol, while each type has unique associated types. The associated type is useful to allow type-safe checks throughout the code base, and the ability to unwrap an existential means I can create a view that displays heterogeneous types conforming types, and each row can get the appropriate data for that row.

We had a system for doing this prior to 5.7, but the new protocol abilities in 5.7 made the code simpler, easier to use, and less prone to errors.

protocol ABTest {
  // Used to identify a test with the server
  var identifier: String { get }

  // The possible options for this test
  associatedtype Options: ABTestOptions
}

protocol ABTestOptions: RawRepresentable<String>, Equatable {}

struct ButtonColorTest: ABTest {
  var identifier: String { "button_color_test" }
  enum Options: String, ABTestOptions {
    case blue
    case purple
  }
}

struct TrialLengthTest: ABTest {
  var identifier: String { "trial_length_test" }
  enum Options: String, ABTestOptions {
    case sevenDays
    case oneMonth
  }
}

struct TestManager {
  private assignments: [String: String] = [:]

  func assignment<T: ABTest>(for test: T) -> T.Options? {
    // Look up what assignment the server has assigned this user
    return T.Options(rawValue: assignments[test.identifier])
  }

  // I can pass an array of 'any ABTest' here
  func fetchTreatments(for tests: [any ABTest]) async {
    self.assignments = assignments(for: tests.map(\.identifier) )
  }
}

// ------ HOW ITS USED ------ //

// I can create an array of `any ABTest` here, which is new with Swift 5.7
let allTests: [any ABTest] = [
  // Declaration for this shorthand elided for sake of space
  .buttonColor, 
  .trailLength
]


// Type-safe checks for individual test assignments
if testManager.assignment(for: .buttonColor) == .blue { ... }
if testManager.assignment(for: .trialLength) == .oneMonth { ... }

// Can construct a view that displays all the different tests
struct ABTestView: View {
  var body: some View {
    ForEach(allTests, id: \.identifier) { test in
      TestRow(test)
    }
  }
}

struct TestRow: View {
  var test: any ABTest

  var body: some View {
    VStack {
      Text(test.identifier)
      ForEach(optionNames(test), id: \.self) { name in
        Text(name)
      }
    }  
  }
 
  // unwrap the existential to get out the specific option names here
  func optionNames<T: ABTest>(_ test: T) -> [String] {
    return T.Options.allCases.map(\.rawValue)
  }
}
1 Like

Just a side note on protocols in Swift 5.7:

In my understanding, with Swift 5.7 you can now have some kind of sum type (in the parlance of algebraic data types) by defining a protocol (that might be empty) and defining according extensions for existing data types. There where some limitations to this idea in Swift <= 5.6, but with Swift 5.7 that should work much better.