/ SWIFT

Opaque Return Types and The 'Some' Keyword in Swift

Opaque types are the Swift type system feature. They specify unnamed but concrete types that implement a specific protocol.

In this article let’s study:

  • What are opaque types?
  • What’s the difference between opaque types, generics, and protocols?
  • When to use opaque types?

Overview

Opaque type can be thought of as “a concrete type that implements this protocol”. Opaque type is denoted with some Protocol in function’s interface:

func makeA() -> some Equatable { "A" }

Although the concrete type is never exposed to function’s callers, the return value remains strongly typed. The reason for that is that the compiler knows the underlying type and preserves it across values from different calls to the function while maintaining the protocol abstraction:

let a = makeA()
let anotherA = makeA()

print(a == anotherA) // ✅ The compiler knows that both values are strings

Let’s test for equality the opaque types, that have different underlying types, but conform to the same protocol:

func makeOne() -> some Equatable { 1 }

let one = makeOne()
print(a == one) // ❌ Compilation error: `a` and `one` are of different types, although both conform to `Equatable`

The compiler does not consider the opaque type to be equivalent to the type it bounds to:

var x: Int = 0
x = makeOne() // ❌ Compilation error: Cannot assign value of type 'some Equatable' to type 'Int'

The function must return the same opaque type every time:

func makeOneOrA(_ isOne: Bool) -> some Equatable { 
    isOne ? 1 : "A" // ❌ Compilation error: Cannot convert return expression of type 'Int' to return type 'some Equatable'
} 

This allows the caller to rely on the fact that the opaque type does not change to a different type in runtime.

The opaque type is equivalent to its underlying type from the compiler’s perspective. The compiler abstracts it away, only exposing the type as something conforming to a given set of constraints.

Opaque Types and Generics

Opaque types are special flavor of generics.

Generics are Swift language feature for type-level abstraction. They allow a type to be used the same way with any other type that satisfies a given set of constraints.

Generics are bound by the caller [1]:

func foo<T: Equatable>() -> T { ... }

let x: Int = foo() // T == Int, chosen by caller
let y: String = foo() // T == String, chosen by caller

Opaque types are bound by the callee:

func bar() -> some Equatable { ... }

let z = bar() // z is abstracted to Equatable. Concrete type is chosen by bar() implementation

Opaque types are sometimes referred to as “reverse generics”.

Opaque Types and Protocols

Opaque types look like protocols and behave a lot like protocols. Therefore, it’s important to justify their difference.

1. You cannot return protocol with Self or associated type requirements from a function. Conversely, it’s possible with the opaque return type:

// Equatable protocol declaration from the Swift standard library
public protocol Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool
}

func makeTwo() -> Equatable { 2 } // ❌ Protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements

func makeThree() -> some Equatable { 3 } // ✅

2. A function can return different protocol types. Conversely, it must return the same opaque type every time:

protocol P {}

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

func makeIntOrString(_ isInt: Bool) -> P { isInt ? 1 : "1" } // ✅

func makeIntOrStringOpaque(_ isInt: Bool) -> some P { isInt ? 1 : "1" } // ❌ Compilation error

When to Use the Some Keyword

The some keyword is particularly useful when designing code for general-purpose use, e.g. a library or a domain-specific language. The underlying type is never exposed to consumers, albeit they can utilize its static features. It allows taking advantage of protocols with associated types and Self requirements since they are resolved on opaque types.

Opaque types make it possible to separate what consumers of your library need to know and the library’s internal implementation.

Summary

Here are the key things to know about Swift opaque types and the some keyword:

  • Opaque type can be thought of as a protocol with private underlying type.
  • Opaque type is defined by the function implementation, but not by the caller.
  • A function must return the same opaque type every time.
  • Use opaque types to utilize associated types and Self-requirements in general-purpose code.

Thanks for reading!

If you enjoyed this post, be sure to follow me on Twitter to not miss any new content.