Expand your APIs capabilities with Opaque Types in Swift

Expand your APIs capabilities with Opaque Types in Swift article title image

Hallo collega’s, Leo hier. The topic today is how to Expand your APIs capabilities with Opaque Types in Swift. Come and learn why the new `some` keyword could be useful for your daily life

Imagine this story: You are asked to write a new SDK that will have a lot of APIs. Your task is to return an object containing the computed result from some calculations and the past results. You want a generic solution that uses the Self in protocol declaration to leverage the solution but still want to use the protocol expression features. How would you do that?

Well, that’s where opaque types come to save the day! With opaque types, you can hide any concrete implementation and provide to the client Self requirement protocols. Let’s see how we do that. And also the limitations of that technique.

Let’s code! But first…

 

Painting of The Day

The masterpiece of today is the 1951 painting called Consciousness of Shock by Victor Brauner. Victor Brauner is the most important painter of the Romanian avant-garde. After his early Impressionist and Expressionist work, he contributed to every avant-garde movement/group. However, most of his oeuvre fits within Surrealism, Brauner being regarded as one of the major pre-and post-war Surrealist painters.

I chose this painting because it is surrealist. Imagine that you receive a gift but it only implements parts of real things, the rest is opaque… it’s surreal, right?

 

The Problem – Expand your APIs capabilities with Opaque Types in Swift

You need to hide some information about your returning API object.

First things first. What are opaque types? The name is very fortunate in this case, opaque types are when you have a concrete type that is made opaque because of a protocol.

The Swift documentation definition is also a good source:

A function or method with an opaque return type hides its return value’s type information. Instead of providing a concrete type as the function’s return type, the return value is described in terms of the protocols it supports. 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. 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 pretty self-explanatory. The initial statement is about how is useful to hide information, so that’s exactly what we are going to do today.

 

Opaque Type Code Example

To implement that in Swift we have some steps:

  1. Create a concrete type.
  2. Create a protocol with only properties and functions you want to expose in your API.
  3. Make your concrete type conforms to the newly created protocol.
  4. Use the created protocol as the return type for your API with the some keyword before.

Let’s start our candy shop example with the code below:

protocol Candy {
    associatedtype Brand
    var brand: Brand { get }
}

struct BubbleGum: Candy {
    var brand: String
}

struct Chocolate: Candy {
    var brand: Int
    var size: Double
}

func createChocolate() -> Candy {
    return Chocolate(brand: 1, size: 10.0)
}

func createBubbleGum(named brand: String) -> Candy {
    return BubbleGum(brand: brand)
}

As you can see is just a protocol with associatedtype called brand and two implementations of it. The first one the brand is a String and the second one the brand is an Int and in the end, functions to construct candies for us.

But there are some errors:

Protocol ‘Candy’ can only be used as a generic constraint because it has Self or associated type requirements

How can we solve that? We just need to add the some keyword to the return of the function call and now the compiler knows that it needs to use the opaque semantics.

For all meaning you are saying: It’s ok the compiler knows the underlying type but the client doesn’t need to know that.

So rewriting the code above:

func createChocolate() -> some Candy {
    return Chocolate(brand: 1, size: 10.0)
}

func createBubbleGum(named brand: String) -> some Candy {
    return BubbleGum(brand: brand)
}

We can test and everything is perfect!

let candy1 = createBubbleGum(named: "Pep Corp")
let candy2 = createBubbleGum(named: "Alan Corp")

dump(candy1)
dump(candy2)

Resulting in:

first result of opaque types in Swift example

 

Opaque Type and Some Superpowers

Now that we added the some keyword to the function description, we can leverage the Candy power.

Imagine that you also want your client to be able to compare Candies, like this:

let candy1 = createBubbleGum(named: "Pep Corp")
let candy2 = createBubbleGum(named: "Alan Corp")

print(candy1 == candy2)

This gives us the error: “binary operator ‘==’ cannot be applied to two ‘some Candy’ operands”. The bright side is that now the Candy protocol can be also Equatable for free for your clients!

protocol Candy: Equatable {
    associatedtype Brand where Brand: Equatable
    var brand: Brand { get }
}

And now you can test them:

let candy1 = createBubbleGum(named: "Pep Corp")
let candy2 = createBubbleGum(named: "Mike Corp")

print(candy1 == candy2) // returns false 
print(candy1 == candy1) // returns true

This also brings compile-time security for your clients:

func createChocolate() -> some Candy {
    return Chocolate(brand: 1, size: 10.0)
}

func createBubbleGum(named brand: String) -> some Candy {
    return BubbleGum(brand: brand)
}

let candy1 = createBubbleGum(named: "Pep Corp")
let chocolate1 = createChocolate()

print(candy1 == chocolate1)

Resulting in the compile-time error:

this is the Expand your APIs capabilities with Opaque Types in Swift error example

Although the return type of createChocolate and createBubbleGum is some Candy, the compiler knows the concrete type and ensures that your clients don’t run into runtime errors comparing different objects.

One interesting thing is that the compiler also ensures that you can only return one type for each function. So it will return the same type every time. You can’t use a boolean to choose what Candy return from within a function, for example:

func createRandomCandy() -> some Candy {
    return Bool.random() ? Chocolate(brand: 1, size: 10.0) : BubbleGum(brand: "Alan Enterprises") // this code doesn't work
}

 

Opaque Types Reversing Generic Logic

With Generics we can write flexible, reusable functions and types that can work with any type, subject to requirements that you define. This means that is the caller who defines the type that will be used.

func doSomething<T:Hashable>(first: T) -> T

In this case, is the caller who will define the type of T.

But when talking about Opaque Types the logic is reversed. The caller doesn’t have power over what will be the concrete implementation, the callee defines that. If you pay attention to our candy shop API when the user calls createChocolate it doesn’t know what will be the concrete implementation of it.

And you can confirm that by printing the object that returns from each function:

dump(chocolate1)
dump(candy1)

Resulting in:

this is the last image for Opaque types in Swift

 

Differences Between Returning a Protocol or an Opaque Type

To wrap up all the content of the blog today, we will check the main differences between returning a protocol and an Opaque Type:

  1. Opaque type can be thought of as a protocol with concrete type that only the compiler knows.
  2. With protocols you can return different underlying concrete types, but with Opaque Types that isn’t possible.
  3. If you need to return a protocol that has Self requirements, like implementing Equatable or any other associated type, you can only use Opaque Types for that.
  4. Use Opaque Types in APIs that need to express more features than the protocol can.

 

Summary – Expand your APIs capabilities with Opaque Types in Swift

That’s it for today, we studied Opaque Types and how they can help you leverage the usage of your protocols in APIs to enable Self requirements. The differences between Opaque Types and Generics. And finally the differences from returning just a protocol.

That’s all my people, I hope you liked reading this article as much as I enjoyed writing it. If you want to support this blog you can Buy Me a Coffee or leave a comment saying hello. You can also sponsor posts and I’m open to freelance writing! You can reach me on LinkedIn or Twitter and send me an e-mail through the contact page.

Thanks for reading and… That’s all folks.

Credits:

title image

Share this post:

Related posts

Featured

Sponsor