Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Super sketchy description of an opaque type alias feature

Opaque Type Aliases


NOTE: This has been abandonded. Only kept around for theoretical historical interest.

NOTE: This is a super sketchy attempt to describe an alternative approach to solve the problems outlined in SE-0244 - Opaque Result Types. This document is intended to be read side-by-side with that proposal.



Also point out that Swift has a generic problem where type erasure is required to pass and return opaque types from public API's. The LLVM's internal APIs and the Swift internal compiler itself have a separation between "public APIs" and private APIs, and it is very common to forward declare a C++ struct or class in the public headers, but only define it in the implementation. Swift has no current solution for this pattern - because the argument and result type of a function must be at least as accessible as the function itself. For example, and internal function cannot return private types.

Proposed solution

Introduce a new decl-modifier named opaque that may be applied to type aliases. This decl modifier says that the type alias itself is exposed with whatever access level the typealias has (public, internal, etc), but the implementation type (the stuff after the equal sign) is not. This means that the aliasee need not itself be a public or internal type (though it is perfectly fine for it to be so) - it can be private or any other access level.

opaque public typealias CertainMutableCollection<T> : MutableCollection & RangeReplaceableCollection = [T]

public func makeMeACollection<T>(with element: T) -> CertainMutableCollection<T> {
   return [element] // ok: an array of T satisfies all of the requirements

The internal Swift grammar supports adding additional constraints through where clauses, but they are currently disabled in typealias declarations. This restriction should be lifted for opaque type aliases, as was described at the end of SE-0048. This is explored in the 'alternatives considered' section.

var c = makeMeACollection(with: 17)
c.append(c.first!) // ok: it's a RangeReplaceableCollection
c[c.startIndex] = c.first! // ok: it's a MutableCollection
print(c.reversed()) // ok: all Collection/Sequence operations are available

func foo<C: Collection>(_ : C) { }
foo(c) // ok: C inferred to be CertainMutableCollection

Moreover, opaque result types to be used freely with other generics, e.g., forming a collection of the results:

var cc = [c]
cc.append(c) // ok: cc's Element == CertainMutableCollection
var c2 = makeMeACollection(with: 38)
cc.append(c2) // ok: Element == CertainMutableCollection

Type identity

An opaque typealias is not considered equivalent to its underlying type by the static type system, unless the code in question is defined within the module that defines the typealias itself, and has access to the aliasee type. For example, from an external module:

var intArray = [Int]()
cc.append(intArray) // error: [Int] is not known to equal the result type of makeMeACollection

However, as with generic type parameters, one can inspect an opaque type's underlying type at runtime. For example, a conditional cast could determine whether the result of makeMeACollection is of a particular type:

if let arr = makeMeACollection(Int.self) as? [Int] {
  print("It's an [Int], \(arr)\n")
} else {
  print("Guessed wrong")

In other words, opaque types are only opaque to the static type system. They don't exist at runtime.

Implementing a function taking and returning opaque types

Define your function and use a type alias to mark whatever you want to be opaque:

protocol P { }
extension Int : P { }
extension String : P { }

// Here we are saying that OpaqueReturn1 is guaranteed to conform to P, but all the other behaviors of String are hidden, and must be explicitly exposed by the API author if they want them.
opaque public typealias OpaqueReturn1 : P = String

public func f1() -> OpaqueReturn1 {
  return "opaque"

// This defines a *different type* that also is known to conform to P.
opaque public typealias OpaqueReturn2 : P = Int

public func f2(i: Int) -> OpaqueReturn2 { // ok: both returns produce Int
  if i > 10 { return i }
  return 0

// Of course the whole point is that we can use private types in the implementation of an opaque typealias.
private struct MyThing : P {}
opaque public typealias OpaqueReturn3 : P = MyThing

public func f3() -> OpaqueReturn3 { return MyThing() }

You can take opaque types as arguments as well, because the compiler knows the identity of types on the caller side:

public func extractField(a : OpaqueReturn1) -> Int {
  // I'm defined in the same module as OpaqueReturn1, so I know it is a string.
  return a.count

This allows clients to use opaque types (passing, returning, forming collections of them, whatever) without being exposed to any more implementation details than the API author intends.

Note that because types have names, and there are no changes to type inference, it is impossible to conflate opaque types with different identities -- you can't even write it:

public func f2(flip: Bool) -> ??? {
  if flip { return 17 }
  return "a string" 

func f3() -> ??? {
  return 3.1419 

func f4() -> ??? {
  let p: P = "hello"
  return p 

Also, because this does not impose any new semantic checking or other behavior change to the compiler (no special rules for returns, no new inference rules, etc), you can of course write recursive functions do not need special support, etc:

func f7(_ i: Int) -> OpaqueReturn1 {
  if i == 0 {
    return f7(1) // obviously ok by existing logic.
  } else if i < 0 {
    let result: Int = f2(-i) // error: cannot convert OpaqueReturn2 to OpaqueReturn1
    return result
  } else {
    return 0 // obviously ok by existing logic.

Similarly, it is of course perfectly fine to call no-return functions that lack a return statement:

func f9() -> OpaqueResult1 {
  // Just fine of course.
  fatalError("not implemented")

Properties and subscripts

These just work by composition, no special rules required.

Associated type inference / naming them

This just works because there is a name, no special rules are required, and unlike the other proposal, clients can write the name specifically without knowing its concrete implementation:

// module A
opaque public typealias SomeP = ...
public func f1() -> SomeP { /* ... */ }

// module B
import A
let vf1 = f1() // type of vf1 is the opaque result type of f1()
let vf12 : SomeP = f1() // Sure, of course you can specify this.

Opaque result types vs. existentials


Detailed design

Grammar of opaque type aliases

This adds a new declaration modifier opaque on typealias. Declaration modifiers do not take keywords, and there are no added grammar productions, even for the 'future directions'.

Restrictions on opaque type aliases

There are no restrictions, because this is not an addition to the type grammar.

Uniqueness of opaque result types

Opaque result types are uniqued based on their declaration, just like all other nominal types and there is no special behavior needed here.

Source compatibility

Same: Opaque type aliases are purely additive.

Effect on ABI stability

Same: Opaque type aliases are purely additive.

Effect on API resilience


Rust's impl Trait

This feature is not based on Rust's design, though it solves some of the same problems.

Alternatives considered

The primary proposal is the major alternative.

Another approach that is more similar to this one is that we could introduce a new opaque type declaration kind instead of using a modifier on typealias:

public opaque SomeCollection<T> : P1, P2 = ...

Pros and cons of this approach is that it makes it less obvious that you could change SomeCollection to be implemented by a different type in the future, and it would take another keyword.

Future Directions

Improve the C importer

Currently when we import opaque types from C (declared in C like struct Foo;) we have no good way to represent these types while preserving their identity. This feature seems like a direct way to model that.

Opaque types in structural position

It follows by composition that this should be allowed:

opaque public typealias SomeCollection : Collection = [Int]
func collection(or not: Bool) -> SomeCollection? {
  if not { return nil }
  return [1, 2, 3]

Furthermore, it trivially follows that there could conceivably be multiple opaque parts of a compound return type:

opaque public typealias SomeOtherCollection : Collection = [String:Int]
func twoCollections() -> (SomeSollection, SomeOtherCollection) {
  return ([1, 2, 3], ["one": 1, "two": 2, "three": 3])

Furthermore, unlike in the original proposal, it is also trivial to express that you're returning the same type in multiple places:

public func twoSameCollections() -> (SomeSollection, SomeSollection) {
  return ([1, 2, 3], [1, 2, 3])

/// client code in a different module:
var (a, b) = twoSameCollections()

// This is ok!  Compiler knows they are the same type, even though it doesn't know what type they are.
a = b

where constraints on associated types of opaque types

This naturally falls out of the existing typealias grammar without adding new _ features and grammar productions:

opaque public typealias ConstrainedCollection<T where T == Int> : Collection<T> = ...
public func foo<T>() -> ConstrainedCollection<T> { ... }

This is described at the end of SE-0048.

Conditional conformances

This directly falls out with no "messy" code or impact on the client, because it is clear to the caller code that the types returned by the two reversed() functions are, in fact, the same.

We would not need to eventually extend the syntax further to support this.

Opaque type aliases

Well yeah, this is covered :-)

Opaque argument types

As described above, this also falls out of the proposal trivially without requiring further type system extensions:

public func foo(x: SomeSollection) { /* ... */ }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment