SE-0244: Opaque Result Types

I went fairly far down the path of designing opaque type aliases, and for me they fell into this mid-point between opaque result types as-proposed and a more powerful forwarding abstraction.

Opaque type aliases are too heavy for the first motivating example in the proposal:

opaque typealias CompactMapCollection<Elements, ElementOfResult>: Collection where .Element == ElementOfResult
  = LazyMapSequence<LazyFilterSequence<LazyMapSequence<Elements, ElementOfResult?>>, ElementOfResult>

extension LazyMapCollection {
  public func compactMap<ElementOfResult>(
    _ transform: @escaping (Elements.Element) -> ElementOfResult?
  ) -> CompactMapCollection<Elements, ElementOfResult> {
    return self.map(transform).filter { $0 != nil }.map { $0! }
  }
}

That opaque type alias CompactMapCollection is difficult to write in the first place. We gained no clarity by writing it out, and we really can't even come up with a name that's more meaningful than "the result of compactMap."

With opaque result types, this would be:

extension LazyMapCollection {
  public func compactMap<ElementOfResult>(
    _ transform: @escaping (Elements.Element) -> ElementOfResult?
  ) -> some Collection where .Element == ElementOfResult
    return self.map(transform).filter { $0 != nil }.map { $0! }
  }
}

The same information is there, but without the extra declaration or the ugly type that needs to be synchronized with the body of compactMap. Note that in both cases I'm assuming we have the where syntax to add additional constraints (on the Element of the hidden type)---you need that in both SE-0244 and the opaque typealias proposal, although it's not outlined or used in the latter. I feel like Greg Titus made this argument well back in the pitch phase; it's part of what made me realize how much we were giving up in simplicity of use when the design started shifting toward opaque type aliases.

On the other hand, it would be nice if it were easy to have a named type that I can implement in terms of another type, but hide my implementation details. An opaque typealias would let me do that in one way, but it means that my named type can only be an alias for some other type---it can't have it's own identity.

Back in the pitch for this proposal, the question "is an opaque type alias really newtype?" came up. newtype is a Haskell construct that creates a new, unique type whose implementation is provided by another type. If you had such a thing in Swift, it would look a lot like opaque type aliases: you would define a new name, provide the (hidden) underlying type, and forward along some protocol conformances to state its initial capabilities. However, unlike with opaque type aliases, you could write an extension on the newtype that would only apply to the new type and not the underlying type, make it conform to protocols on its own, etc.

But once you look at newtype, you realize that it's not all that different from declaring a new struct with a single stored property (the underlying value) and then forwarding conformances along. @anandabits has a proposal draft for protocol forwarding that demonstrates this (it predates the switch to Discourse, so the formatting is a mess) that I find compelling. For example, it would help you implement a Meters type as a struct containing a Double, and then forward the FloatingPoint APIs so you get a unique type that let's you perform numeric computation with just a few lines of code.

For me, opaque type aliases fall into this uncanny middle ground between "lightweight way to hide a result type that's hard to write and reason about" and "useful feature for building new types." It solves neither problem well, but if it's there people will reach for it and not get what they want.

Doug

15 Likes