Xcoding with Alfian

Software Development Videos & Tutorials

Understanding Opaque Return Types in Swift

Alt text

Opaque return types is a new language feature that is introduced in Swift 5.1 by Apple. It can be used to return some value for function/method, and property without revealing the concrete type of the value to client that calls the API. The return type will be some type that implement a protocol. Using this solution, the module API doesn’t have to publicly leak the underlying internal return type of the method, it just need to return the opaque type of the protocol using the some keyword. The Swift compiler also will be able to preserve the underlying identity of the return type unlike using protocol as the return type. SwiftUI uses opaque return types inside its View protocol that returns some View in the body property.

Here are some of the essential things that opaque return types provides to keep in our toolbox and leverage whenever we want to create API using Swift:

  1. Provide a specific type of a protocol without exposing the concrete type to the API caller for better encapsulation.
  2. Because the API doesn’t expose the private concrete return type to it’s caller, the client doesn’t have to worry if in the future the underlying types gets changed as long as it implements the base protocol.
  3. Provides strong guarantees of underlying identity by returning a specific type in runtime. The trade off is losing flexibility of returning multiple type of value offered by using protocol as return type.
  4. Because of the strong guarantee of returning a specific protocol type. The function can return opaque protocol type that has Self or associated type requirement.
  5. While the protocol leaves the decision to return the type to its caller of the function. In reverse for opaque return types, the function itself have the decision for the specific type of the return value as long as it implements the protocol.

Diving into the example of using Opaque return types

To understand more about the opaque return type and why it is different than just using protocol as return type, let’s dive into some code examples on how we can use it.

Declaring Protocol with associatedtype

Let’s say we have a protocol called MobileOS . This protocol has an associatedtype called Version and a property to get the Version for the concrete type to implement.

protocol MobileOS {
    associatedtype Version
    var version: Version { get }
    init(version: Version)
}

Implementing concrete types of the Protocol

Let’s define two concrete types for this protocol, which is iOS and Android. Both of them has different version semantics. The iOS uses float type while Android uses String (although they just changed it recently in Android 10 😋).

struct iOS: MobileOS {
    var version: Float
}struct Android: MobileOS {
    var version: String
}

Create Function to return the Protocol type

Let’s say we want to create a new function that returns the MobileOS protocol as the return type. The naive approach is to write it like this:

Solution 1 (Returns Protocol Type):

func buildPreferredOS() -> MobileOS {
    return iOS(version: 13.1)
}// Compiler ERROR 😭
Protocol 'MobileOS' can only be used as a generic constraint because it has Self or associated type requirements

As you can see, the compiler fails to build because our protocol has a constraint that uses associatedtype. The compiler doesn’t preserve the type identity of the returned value when using protocol as return type. Let’s try another solution which is returning the concrete type directly.

Solution 2 (Returns Concrete Type):

func buildPreferredOS() -> iOS {
    return iOS(version: 13.1)
}// Build successfully

This solution works, but as you can see the API now leaking the concrete type to the caller. This code will be need so much refactoring if in the future we are changing our minds and returns Android as the return type of the function.

Solution 3 (Generic Function Return)

func buildPreferredOS<T: MobileOS>(version: T.Version) -> T {
    return T(version: version)
}let android: Android =  buildPreferredOS(version: "Jelly Bean")
let ios: iOS = buildPreferredOS(version: 5.0)

Yes, this approach works elegantly. But now the caller of the API needs to provide the concrete type of the returned function. If we truly want to make the caller doesn’t have to care about the concrete return type, then this is still not the correct solution.

Final Solution (Opaque Return Type to the rescue 😋:)

func buildPreferredOS() -> some MobileOS {
    return iOS(version: 13.1)
}

Using the opaque return type, we finally can return MobileOS as the return type of the function. The compiler maintains the identity of the underlying specific return type here and the caller doesn’t have to know the internal type of the return type as long as it implements the MobileOS protocol

Opaque returns types can only return one specific type

You might be thinking that like the protocol return type, we can also return different type of concrete value inside an opaque return type like so.

func buildPreferredOS() -> some MobileOS {
   let isEven = Int.random(in: 0...100) % 2 == 0
   return isEven ? iOS(version: 13.1) : Android(version: "Pie")
}// Compiler ERROR 😭
Cannot convert return expression of type 'iOS' to return type 'some MobileOS'func buildPreferredOS() -> some MobileOS {
   let isEven = Int.random(in: 0...100) % 2 == 0
   return isEven ? iOS(version: 13.1) : iOS(version: "13.0")
}// Build Successfully 😎

The compiler will raise a build time error if you are trying to return different concrete type for opaque return value. You can still return a different value of the same concrete type though.

Simplify complex and nested type into an opaque return type for the API Caller

The final example of opaque return value is a really a good example of how we can leverage Opaque return type to hide complex and nested type into a simple opaque protocol type that can be exposed to the client.

Consider a function that accepts an array that uses generic constraint for its element to conform to numeric protocol. This array performs several things:

  1. Drop the head and tail elements from the array.
  2. Lazily map the function by doing multiply operation of itself for each element.

The caller of this API doesn’t need to know the return type of the function, the caller just want to able to perform for loop and print the value of the sequence to the console.

Let’s implement this naively using a simple function.

Solution 1: Using Generic Return Function

func sliceFirstAndEndSquareProtocol<T: Numeric>(array: Array<T>) -> LazyMapSequence<ArraySlice<T>, T> {
   return array.dropFirst().dropLast().lazy.map { $0 * $0 }
}sliceFirstAndEndSquareProtocol(array: [2,3,4,5]).forEach { print($0) }
// 9
// 16

As you can see the return type of this function is very complex and also nested LazyMapSequence<ArraySlice<T>,T> , while the client only using it to loop and print each element.

Solution 2: Simple Opaque Return Types

func sliceHeadTailSquareOpaque<T: Numeric>(array: Array<T>) -> some Sequence {
    return array.dropFirst().dropLast().lazy.map { $0 * $0 }
}sliceHeadTailSquareOpaque(array: [3,6,9]).forEach { print($0) }
// 36

Using this solution, the client doesn’t have to know about the underlying return type of the function as long as its conforms to the sequence protocol that can be used by the client.

Opaque Return Types in SwiftUI

The SwiftUI also relies heavily on this approach so the the body in the View doesn’t have to explicitly reveal the concrete return type as long as it’s conform to the View protocol. Otherwise the inferred return type can be very nested and complex like this.

struct Row: View {
    var body: some View {
        HStack {
           Text("Hello SwiftUI")
           Image(systemName: "star.fill")
        }
    }
}

The inferred return type of the body is:

HStack<TupleView<(Text, Image)>>

It’s pretty complex and nested 🤯, remember it will also change whenever we add a new nested view inside the HStack . The opaque return type really shines in SwiftUI implementation, the client of the API doesn’t really care about the underlying internal concrete type of the View as long as the return type conforms to the View protocol.

Learning more about Opaque return type

To learn more about opaque return type, you can follow the link that i provide below. They are official proposal, documentation and video from Apple:

Conclusion

It’s been a very amazing and incredible journey for us Swift developers that has been learning and adopting Swift since its inital release in WWDC 2014. Remembering back then, the language doesn’t even have some amazing features that opens a new paradigm of building an API like protocol extension, generic in protocol with associatedtype, and even cool features like Codable protocol to simplify the decoding and encoding of a model to specific data type.

Only because Apple releases Swift as an open source programming language that we can harness the power of collectiveness from developers all around the world that want to improve the language through Swift Evolution proposals. There is even a proposal implementation that has been accepted to Swift Language by a high schooler (default synthesized initalizer for struct in Swift 5.1 by Alejandro Alonso 👏🏻).

The impact of Swift technology to the world ahead can be very significant as the language matures ahead. It is also the programming language that i love the most because of its expressiveness. At last, keep on learning and have beginner mindset, don’t be afraid to fail, learn from your failure, try and repeat. Keep the lifelong learning goes on and happy Swifting!!!