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:
- Create a concrete type.
- Create a
protocol
with only properties and functions, you want to expose in your API. - Make your concrete type conforms to the newly created
protocol
. - Use the created
protocol
as the return type for your API with thesome
keyword before.
Let’s start our candy shop example with the code below:
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:
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:
Resulting in the compile-time error:
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:
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
:
- Opaque type can be thought of as a protocol with concrete type that only the compiler knows.
- With protocols you can return different underlying concrete types, but with Opaque Types that isn’t possible.
- 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. - 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: