Allow opaque result types in protocol requirements

I'm trying to model a generic transformation. There should be a protocol requirement, and that requirement accepts a generic collection and returns some Collection of transformed contents.

For instance, the transformation may return a LazyMapCollection:

func transform<Source>(
  _: Source
) -> LazyMapCollection<Source, UInt8> where Source: Collection, Source.Element == UInt8 {
  // ...
}

My understanding is that this is a sort of "higher-kinded type" - Source is used both by input type and output type. Indeed, it is impossible to express this today in Swift.

protocol Transformer {
  associatedtype TransformedData: Collection where EncodedData.Element == UInt8
  func transform<Source>(_: Source) -> TransformedData where Source: Collection, Source.Element == UInt8
}

struct MyTransformer {

  // 'TransformedData' would be inferred as LazyMapCollection<Source, UInt8>
  // from the function signature, but that clearly doesn't make sense here
  // because we don't know what 'Source' is.
  typealias TransformedData = ???

  func transform<Source>(
    _: Source
  ) -> LazyMapCollection<Source, UInt8> where Source: Collection, Source.Element == UInt8 {
    // ...
  }
}

This is very limiting. This function cannot return any generic wrappers at all - it will have to copy data from all Source types to a single type (e.g. an Array), or erase the LazyMapCollection in an existential. In summary, it must ensure that Source no longer appears on both sides of the function signature:

struct MyTransformer {

  typealias TransformedData = AnyCollection<UInt8>
  //                          ^^^^^^^^^^^^^^^^^^^^
  //                          no longer mentions 'Source'

  func transform<Source>(
    _: Source
  ) -> AnyCollection<UInt8> where Source: Collection, Source.Element == UInt8 {
    // ^^^^^^^^^^^^^^^^^^^^ - no longer mentions 'Source'
  }
}

But there is, in theory, another option. Why couldn't we use an opaque type? This would totally work as a free function:

func transform(_: some Collection<UInt8>) -> some Collection<UInt8>

And it does what we want - it breaks the type dependency between the function's input and output, but in a way that allows the implementation to avoid copying or existential boxing. However, this appears to be banned in protocol requirements:

protocol Transformer {
  func transform(_: some Collection<UInt8>) -> some Collection<UInt8>
  // error: 'some' type cannot be the return type of a protocol requirement; 
  // did you mean to add an associated type?
}

This limitation is mentioned in SE-0244, but I don't find the reasoning to be satisfactory (emphasis added):

More fundamentally, opaque result types cannot be used in the requirements of a protocol:

protocol Q { func f() -> some P // error: cannot use opaque result type within a protocol }

Associated types provide a better way to model the same problem, and the requirements can then be satisfied by a function that produces an opaque result type. (There might be an interesting shorthand feature here, where using some in a protocol requirement implicitly introduces an associated type, but we leave that for future language design to explore.)

I would like to suggest that associated types cannot always be used in place of opaque types, and that there are valuable use-cases for opaque result types in protocol requirements beyond simply being shorthands for associated types.

Again, the function I am trying to express works already as a free function, so I don't see any reason why it could not also be a protocol requirement. Is there some implementation reason why it cannot be supported?

If possible, I would like us to lift this restriction. There are interesting models which we cannot express today.

6 Likes

The requirement does not have to match the implementation. You can write the requirement the old way and the implementation the new way and rely on associated type inference.

I don’t think it would break the language to support the other way for protocols, either. It would be effectively shorthand for the old way, except that the associated type wouldn't have a name you can write in source. But I don’t know if we need it, either; I’d like to see more motivation first. (We could definitely add a fix-it, though.)

…oops, strike ALL of that, I see the problem now. This would indeed be a new feature.

EDIT: to spell it out clearly, a normal associated type is a function of Self (e.g. “String.Subsequence = Substring”). A normal opaque result type for a protocol extension method is a function of the input generic types including Self (e.g. “String.lazy.map produces a LazyMapCollection<String, ResultElement> even if you can’t see it”. A requirement opaque result type would be more like the protocol extension opaque result type case than an associated type.

3 Likes

swift-grammar’s ParsingRule has a similar problem, where it has a requirement parse(_:) which can take many different kinds of input, but must return a fixed Construction type. the solution i went with was to add more associatedtype axes to ParsingRule, to accomodate generic-ness in the parsing output.

the doccomment for the associatedtype Location explains the workaround in more detail.

in your example, this would mean making MyTransformer generic, and using the generic parameter as the type witness.

1 Like

Right. So, if an opaque type is defined by its generic environment, then for a non-generic protocol requirement (say, SwiftUI's View.body), an opaque return type is the same as an associated type (the generic environment is just Self).

But in a generic protocol requirement, there are additional elements that make up the generic environment (in this case, a Source collection). In a way, it is similar to a generic associated type.

If we had GATs, the transformer could be written as follows:

protocol Transformer {
  // Generic associated type
  associatedtype TransformedData<Source>: Collection
  where Source: Collection, Source.Element == UInt8,
        TransformedData.Element == UInt8

  func transform<Source>(
    _: Source
  ) -> TransformedData<Source>
}

This would more rigorously model the relationship between the function's input and return types, possibly allowing for additional constraints (e.g. we could say that TransformedData and Source must have the same Index type, if we cared about that).

In this case, I don't need to preserve that information, so we don't need to get in to the whole business of expanding the type system to handle those. An opaque result type would do just fine.

Also worth mentioning: opaque result types can also be defined by availability conditions now (not just their generic environments), so there is an additional way in which they are not the same as associated types.

4 Likes

SwiftUI’s ViewModifier is another example of where generic associated types would be useful. Currently, the makeBody requirement accepts an existential so that the return type can be a normal associated type. I think the intended API was for makeBody to be generic over its input in addition to being opaque.

+1

I hit this just today and I'm surprised Swift doesn't allow that.

protocol Storage {
    func list<T: StorageableEntity>(_ objectsOf: T.Type) async throws -> some Sequence<T>
}