Swift — Opaque types

Introduced with Swift 5.1, opaque types take protocols with associated types to a whole new level.

They enable us to use generic protocols as functions’ return types while keeping the concrete type information private.

Hiding type information is useful at boundaries between a module and code that calls into the module, because the underlying type of the return value can remain private.

Before Swift 5.1, defining such an implementation would raise an error because the compiler would not be able to infer the underlying concrete type.

protocol Instrument {
associatedtype NewInstrument where NewInstrument: Instrument
init()
func play()
func swap() -> NewInstrument
}
struct Piano: Instrument {
func play() {
print(“Playing with my brand new piano”)
}
func swap() -> Guitar {
Guitar()
}
}
struct Guitar: Instrument { func play() {
print(“Playing with my brand new guitar”)
}
func swap() -> Piano {
Piano()
}
}
func build() -> Instrument {}// Error : Protocol ‘Instrument’ can only be used as a generic constraint because it has Self or associated type requirements

Using the some keyword will now address the issue letting the compiler automatically infer the concrete type from within the function’s body.

func build() -> some Instrument {
Guitar()
}

The callers of the build() function won’t know the exact return type, only the compiler will.

let instrument = build()

The constant declaration clearly indicates let instrument: some Instrument.

Yet, under the hood, the instrument is a Guitar type.

type(of: instrument)
// Guitar

Unlike returning a value whose type is a protocol type, opaque types preserve type identity — the compiler has access to the type information, but clients of the module don’t.

This is a major improvement in the Swift language as it enable us to deal with generalized types keeping the underlying types private.

The latter comes in handy for complex return types which we don’t really want to expose to the public such as Slice<LazySequence<[String]>> in the following implementation.

extension Sequence where Element == Int {
func transformed() -> some Sequence {
self
.filter { $0.isMultiple(of: 2) }
.map { "\($0)" }
.lazy
}
}

Using an opaque return type will simply return some Sequence.

Yet, in the implementation details, the compiler knows exactly what concrete type to use that is a Slice<LazySequence<[String]>> type.

let transformed = [1, 2, 3, 4].transformed()
// some Sequence

Back to our Instrument protocol, our build() function is supposed to return some Instrument however our implementation will always return a Guitar.

Our functions should build a specific instrument.

func buildGuitar() -> some Instrument {
Guitar()
}
func buildPiano() -> some Instrument {
Piano()
}
let guitar = buildGuitar()
let piano = buildPiano()

Both of our constants will be typed some Instrument however when trying to use them in an array, the compiler will raise an error.

let instruments = [guitar, piano]// Error: Heterogeneous collection literal could only be inferred to ‘[Any]’; add explicit type annotation if this is intentional

It makes sense since the underlying types are different.

Our implementation induces a lot of code duplication, it looks like a great place to use generics.

func build<T: Instrument>() -> T {
T()
}
let guitar: Guitar = build()
let piano: Piano = build()

Much better however we don’t use opaque types letting the callers choose the concrete type which is an undesired behavior.

Returning the some Instrument opaque type should address the issue.

func build<T: Instrument>() -> some Instrument {
T()
}
let guitar: Guitar = build()
let piano: Piano = build()
// Error: Generic parameter ‘T’ could not be inferred

Well.. it doesn’t.

When returning opaque types, the underlying type must be inferred by the compiler within the function’s body so the concrete type must be clearly defined there.

As far as the compiler knows, the build() function could return any kind of instruments, it has no clue what concrete type to return hence the error.

You can think of an opaque type like being the reverse of a generic type.

Generic types let the code that calls a function pick the type for that function’s parameters and return value in a way that’s abstracted away from the function implementation.

This is key to understanding how opaque and generic types differ.

As mentioned in the Swift documentation,

Returning an opaque type looks very similar to using a protocol type as the return type of a function, but these two kinds of return type differ in whether they preserve type identity.

An opaque type refers to one specific type, although the caller of the function isn’t able to see which type; a protocol type can refer to any type that conforms to the protocol »

The decision to choose between opaque types or protocols as returned types will be tightly related to whether or not you need to hide the type information.

Check out my articles about generics and protocol-oriented programming to better understand opaque types.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store