Unlock Existential Types for All Protocols

This proposal has been scheduled as SE-0309, and the review will run from April 17 through May 1.

15 Likes

Glad to hear it!

…what’s the etiquette for non-authors to fix typos (misspelled words and/or grammatical errors) in a proposal document?

I suggest submitting a PR directly into the proposal to fix typos etc. in other threads people did way too many single posts to point out single typos which added noise to the review :)

5 Likes

All right, done.

There’s also a line in the proposal which reads, “Tuple types in either of their element types” where I’m pretty sure that “either” should be “any”, but it’s not per se a typo so I didn’t include it in my PR so as not to overstep.

2 Likes

Hi, isn't the review thread coming today?

Yes, the review thread is now open.

3 Likes

Asking here because I just don’t understand and don’t want to muddy the review thread. In the AnyFoo example:

struct AnyFoo<Bar>: Foo {
  private var _value: Foo

  init<F: Foo>(_ value: F) where F.Bar == Bar {
    self._value = value
  }
  
  func foo(_ bar: Bar) -> Bar {
    return self._value._fooThunk(bar)
  }
}

Why can’t _value have its Bar constrained? Like:

private var _value: Foo Where _value.Bar == Bar

Is that just a missing feature that can be added later? Or is there a technical reason that can’t happen?

Thanks!

I think your request relates to the following future direction mentioned at the end of the proposal:

3 Likes

Thank you, I'll fix that up.

P.S. I believe everyone should feel free to submit these PRs (and request a review if necessary).

2 Likes

Today it is not possible to cast to an existential when the protocol of that existential type has a Self or associated type requirement.

protocol A {
  associatedtype A
}

object as? A // error: Protocol 'A' can only be used as a generic constraint because it has Self or associated type requirements

If this proposal is adopted and integrated into this language, would the above cast compile?

My understanding is that the answer is "yes" but I haven't been able to find explicit confirmation in the proposal or in the discussion on the pitch posts.

2 Likes

Yes. as? expects a type on the right-hand side, and this change will allow A to be used as a type.

10 Likes

Let me provide some examples from the projects I was working on:

  1. AnyEncodable
extension URLRequest {
  mutating func encoded(encodable: Encodable, encoder: JSONEncoder = JSONEncoder()) throws -> URLRequest {
    httpBody = try encoder.encode(encodable) // Error: Protocol 'Encodable' as a type cannot conform to the protocol itself
  }
}

Here we need AnyEncodable type erasure struct. It will be good if existentials become self conforming, as Error protocol do.

  1. AnalyticsSender
public protocol StatisticsSender: AnyObject {
  func sendEvent(_ event: StatisticsEvent)

  ... other functions and properties
}

public protocol StatisticsSDKSender: StatisticsSender {
  // Different SDKs use different types as value Type of event payload: [String: NSObject] / [String: Any] / [String: String]. So NativeValue becomes NSObject, Any or String in implementation.
  associatedtype NativeValue

  func stringRepresentation(ofEventName eventName: StatisticsEventName) -> String?
  ... other functions and properties
}

Currently we have to split protocol in two different, because we can not create Array<StatisticsSDKSender>.
Another option is to create type erasure struct. Honestly, I like none of them. Array<StatisticsSDKSender> would be great. What we need is:

  • keep strong references to senders in array.
  • be able to call methods, that don't depend on associated value (NativeValue in provided example). Such as func stringRepresentation(ofEventName:) -> String
  1. AnyCollection<StatisticsEventParam>
    Analytics event has a property with params. Now its type is AnyCollection.
    We don't use Array, because it is not suitable. Fo example, several weeks ago we began to use OrderedSet from SwiftCollections library.
    Opaque types don't help here, because the don't provide element type. At least in the way they work now

Here we need something like this:

  var params: Collection where Element == StatisticsEventParam { get }

  // or ideal variant
  var params: some Collection where Element == StatisticsEventParam { get }
  1. AnyRouter
    Situation is equal to StatisticsSDKSender. Several protocols instead of one.

  2. AnyObserver
    public struct AnyObserver: ObserverType {}

Good if we are able to create collection of ObserverType:

class MainViewModel {
    var observers: Set<ObserverType> where Element == Response
}

  1. AnyViewModelConfigurable
protocol AnyViewModelConfigurable: ViewModelConfigurable {
  associatedtype ViewModel

  func configure(with viewModel: ViewModel)
}

Situation is equal to StatisticsSDKSender. Several protocols instead of one.

  1. AnyEquatable
    It is interesting case.

@Joe_Groff says: "different dynamic types can be compared by wrapping both in AnyHashable"
From my point of view comparing two instances of different types via wrapping them by AnyHashable is language defect.
Opaque types provide this in type safe manner.
If we create Array, then put there String, Int and say, CLLocation, then pass it somewhere and become to compare, it is very strange.

"Equatable and Hashable existentials ought to be something we handle as a special case" - I can hardly imagine reasons for this. It will be interesting to know them.

Seems we can make both variants possible.

  1. Equatable and Hashable behave as all other PAT protocols
  2. AnyHashable struct continues to live in Standard Library. Those, who want to compare different types, can wrap them in AnyHashable.

I don't think Encodable is a good example for self-conformance. I would expect self-conformance of Encodable to encode as a container - serialising the type of the value together with the serialised value.

I think that's more of an example for the need of opening existentials:

extension URLRequest {
  mutating func encoded(encodable: Encodable, encoder: JSONEncoder = JSONEncoder()) throws -> URLRequest {
    let <T: Encodable> value: T = encodable
    httpBody = try encoder.encode(value)
  }
}

Why is even that necessary? You can just make the function generic, no need to erase or open anything. AnyCodable is useful for heterogenous collections, but this isn't an example of that requirement.

Unfortunately, it can not be generic.

/// Represents an HTTP task.
public enum Task {
  case requestPlain
  
  case requestData(Data)
  
  /// A request body set with `Encodable` type
  case requestJSONEncodable(Encodable)
}

Task is used in different network providers, it is impossible to make it generic, such as Task<T>.
In case we use requestJSONEncodable(Encodable), we use type erased Encodable. But try encoder.encode(value) function needs a concrete Type, so type erasure struct is needed.

For more details, see Moya: GitHub - Moya/Moya: Network abstraction layer written in Swift.

Yeah, Moya has some unfortunate abstractions that prevent it from working well in these situations. A router that goes directly to the URLRequest can handle this much more easily, as you can assign a specific type per route, or use other abstractions. Raw use of Encodable should be avoided for that reason. I do wonder if this case will be helped by this proposal though.

It will be off-topic, but maybe you can use extension of Encodable.

private extension Encodable {
    func encoded(encoder: JSONEncoder = JSONEncoder()) throws -> Data {
        return try encoder.encode(self)
    }
}

With this, you can use Encodable.encoded(encoder:), so you can write this. As far as I checked your repository, you don't have to use AnyEncodable with this extension.

extension URLRequest {
  mutating func encoded(encodable: Encodable, encoder: JSONEncoder = JSONEncoder()) throws -> URLRequest {
    httpBody = try encodable.encoded(encoder: encoder)
  }
}

Can you please clarify, why raw use of Encodable should be avoided?

Here we talk about different approaches for making network layer abstractions. Each one has its own trade-offs.
My point here is that more expressivity gives us a choice in making such abstractions.

Thanks for this great example. Yes, this is a good workaround.
Interesting thing here is that really concrete Type isn't known to the compiler, and Encodable behaves like a self conforming protocol:

extension Encodable {
    func encoded(encoder: JSONEncoder = JSONEncoder()) throws -> Data {
        return try encoder.encode(self) // encodable is used here as self conforming protocol, concrete type isn't known
    }
}

So why it is not possible to pass Encodable existential directly to encoder.encode(_:) function now? This is the question. Seems like an unnecessary limitation, because writing a function in extension do the thing.

1 Like

Also there are situations, where this trick is impossible.

let result: Result<String, LocalizedError> // compiler error

extension LocalizedError {
  // compiled, can be called inside extension, but can not be called outside 
  func makeResult<T>(_ t: T.Type) -> Result<T, Self> {
    .failure(self)
  }
    
  func performFailureCompletion<T>(_ t: T.Type, completion: @escaping (Result<T, Self>) -> Void) {
    let result = makeResult0(t.self, error: self)
    completion(result)
  }
  
  func process() {
    performFailureCompletion(Int.self) { result in
      // result here is of Type Result<String, LocalizedError>, which can not be created directly. But here it exists
      print(result)
    }
  }
}

Having Result with Failure of type LocalizedError or any other Error Type is very convenient and often necessary. In real project many of Error-type protocols can exist. Creating type erasure struct for each one is unpleasant.