Back to blog

Opaque Types and ‘some’ in Swift


Aasif Khan
By Aasif Khan | Last Updated on February 17th, 2022 6:34 am | 5-min read

Opaque types are an important feature of Swift. With the some keyword, which denotes an opaque type, you can “hide” the concrete return type of a computed property or function. And that allows us to write flexible, concise and robust Swift code.

In this tutorial, we’re going to discuss how opaque types work. Here’s what we’ll get into:

  • How do you use the some keyword?
  • What’s an opaque type and why do we need it?
  • How do opaque types affect SwiftUI and iOS development?
  • 3 advantages of the some keyword in Swift
  • Protocols vs. opaque types, and associated types

Opaque Types: It’s “some” Thing ¯\_(ツ)_/¯

Since Swift 5.1, you can mark a type with the some keyword. You might have seen it in a SwiftUI view, like this:

struct MyFirstView: View {
var body: some View {
Text(“Hello worl!”)
}
}

See that some keyword in there, right before the type View of the body computed property? Yup, that’s the one!

There’s quite a bit of magic behind that some keyword. In the next few sections, we’re going to dissect how some works, what kinds of problems it solves, and why it’s useful in practical iOS development.

Before we find out what’s going on exactly, let’s start with an accurate but incomplete description of what some is.

The some keyword indicates that a value, such as the body computed property, has an opaque type. We’re hiding type information (“opaque”) from the code that uses MyFirstView.

The implementation of the body property, i.e., the stuff between the squiggly brackets, determines the concrete type of the body property. This type isn’t exposed to the code that uses MyFirstView; it remains private.

Confusing? It sure is. Don’t worry if any of this doesn’t make sense yet – it will make sense by the end of this tutorial. Let’s dive in!

Opaque Types and Generics

Opaque types and generics are closely related. In fact, an opaque type is often described as a reverse generic type.

A quick comparison:

  • With a generic type, the caller of the function determines the concrete type of the placeholder (“outside”).
  • With opaque types, the implementation determines the concrete type (“inside”).

Let’s back up a bit. Remember generics? Generics let you define placeholder types, and constraints, so that a function can accept different types, while remaining strong typed. It makes your code more flexible, while staying safe.

Take a look at this example:

func addition(a: T, b: T) -> T {
return a + b
}

// Adding two integers
let resultA = addition(a: 42, b: 99)

// Adding two doubles
let resultB = addition(a: 3.1415, b: 1.618)

In the above code, we’re using the generic placeholder T. When parameters a and b are both of type T, the function returns a value of type T, provided that the T type conforms to the Numeric protocol. As a result, we can use this function to add integers, doubles, floats, etcetera.

It’s important to understand 2 things at this point:

  1. The type placeholder T is really a placeholder. When the code compiles, Swift will replace it with concrete types, such as Int and Double. When you test the type of a or b at runtime, you’ll see that they will have concrete types, i.e. Int, Double, etc.
  2. The caller† of the addition(a:b:) determines the concrete type of placeholder T. It’s telling the function: “Look, you’re using integers now, because I say so.” You could say that the placeholder is transparent, because the caller knows what the underlying type of T is.

†: A call site here is the line that starts with let resultA , which means that the integer literal 42 determines that T is an Int (integer) for that call.

Let’s compare this to opaque types. Take a look at the following example code. First, we’ll define a protocol Shape and two shapes that conform to the protocol.

protocol Shape {
func describe() -> String
}

struct Square: Shape {
func describe() -> String {
return “I’m a square. My four sides have the same lengths.”
}
}

struct Circle: Shape {
func describe() -> String {
return “I’m a circle. I look like a perfectly round apple pie.”
}
}

Then, we’re going to define a function makeShape(), like this:

func makeShape() -> some Shape {
return Circle()
}

The makeShape() returns a value of type Shape. It uses the some keyword to denote that this is an opaque type. It’s up to the function to determine what concrete type is returned. In the above code, we’re returning a value of type Circle.

let shape = makeShape()
print(shape.describe())
// Output: I’m a circle. I look like a perfectly round apple pie.

In the above code, we’ve simply called the makeShape() function and printed out the result of its describe() function. So far so good! The output is exactly like we expected, because the text from the Circle shape is printed out.

At this point, you may wonder why we didn’t just use a protocol. After all, if you remove the some keyword, the above code still runs perfectly fine. Why use some at all?

Opaque types and protocol types differ in an important way: opaque types preserve type identity, and protocol types don’t.

A quick comparison:

  • An opaque types always refers to one specific, concrete type – you just don’t know which one.
  • A protocol type can refer to many types, as long as they conform to the protocol.

Differently said, a protocol type gives you more flexibility about the type that’s returned. An opaque type lets you be more strict about the type that’s returned. We’ll get back to protocol types vs. opaque types later.

But… there’s more!

Protocol Types and Associated Types

Protocols can have associated types. An associated type gives a placeholder name to a type that’s used as part of the protocol. For example, Swift’s Collection protocol has an associated type called Element. It corresponds to the type of the elements inside the collection, such as String inside [String] (an array of strings).

Such a placeholder is the same placeholder type as found in generics, so they’re not concrete types – just placeholders. Associated types are useful when you want to define a value in a protocol, but you don’t want to be specific about the type of that value. Associated types need to be made concrete by the type that adopts the protocol.

Note: You can learn more about associated types in this tutorial about generics. In it, we’re using a protocol with an associated type to design any kind of storage that can store any kind of item.

Let’s add an associated type to the Shape protocol we’ve used before. Like this:

protocol Shape {
associatedtype Color
var color: Color { get }
func describe() -> String
}

In the above Shape protocol, we’ve added an associated type Color. We’re using it as the type of the color property. Note that the type Color doesn’t exist, it’s just a placeholder!

Next up, we’re creating two implementations of the Shape protocol. Like this:

struct Square: Shape {
var color: String
func describe() -> String {
return “I’m a square. My four sides have the same lengths.”
}
}

struct Circle: Shape {
var color: Int
func describe() -> String {
return “I’m a circle. I look like a perfectly round apple pie.”
}
}

What’s happened? We’ve created a Square and a Circle struct, like before, which both adopt the Shape protocol. Both shapes also implement the color property, as required by the protocol. Their types are different, though!

We’ve given the color properties a concrete type:

  • Square uses a string for color
  • Circle uses an integer for color

For the sake of the example, it’s easiest to imagine at this point that we can describe a color as a string, like “Green”, and as a number, like 255.

Finally, we want to build a function that produces a shape. We don’t care what kind of shape, so we’re using the Shape protocol as its return type. It can return anything that conforms to the Shape protocol.

Like this:

func makeShape() -> Shape {
return Square(color: “Yellow”)
}

When you run the above code, you get a compile-time error that says: Protocol ‘Shape’ can only be used as a generic constraint because it has Self or associated type requirements. Wait, what?

This cryptic error is hard to decode, but what it means is that Swift cannot concretize the associated type Color. Based on the function declaration, the return type of the function is Shape. Such a Shape has an associated type Color, which is used for its color property. But what is the concrete type of the color property?

Let’s find out.

Don’t assume that this error pops up because of those disparate types of the color properties. The problem here is that the protocol has an associated type, and Swift cannot work out its concrete type.

Opaque Types to the Rescue

In the code we’ve written so far, what’s the concrete type of Color, the associated type of the Shape protocol?

func makeShape() -> Shape {
return Square(color: “Yellow”)
}

We know, of course. You can clearly see that, based on the implementation of the makeShape() function, the concrete type for Color is String.

The shape we’re working with is Square, and the color property of Square uses the String type. However, Swift can’t rely on this information because it’s part of the implementation of the function, and not of its function declaration.

Check this out:

func makeShape() -> Shape { // <-- Swift doesn't know type of `Color` here return Square(color: "Yellow") // <-- it does know type of `Color` here! } Swift can’t be certain that the makeShape() function will always return a Shape of which the associated type is String. For all it knows, the associated type could be Int or Cowbell or Invoice. Unfortunately, the associated type for Shape cannot be determined by “looking into” the function implementation. So, what do we do? We add the some keyword to the function declaration, like this: func makeShape() -> some Shape {
return Square(color: “Yellow”)
}

The some keyword will make the Shape return type opaque. Instead of makeShape() returning any type that conforms to the Shape protocol, it can now return a type that conforms to the Shape protocol – always the same one, but we don’t know which one.

Just as with generic type placeholders, the Shape type is concretized when Swift compiles the code. The concrete type for some Square is Square here, based on the implementation of the function (i.e., code inside function).

Just as with generic placeholder types, we, the developers, get to be generic and opaque about what type we’re returning exactly. It’s always the same type, we just don’t (yet) know which one.

We’ve come full circle! A quick recap:

  • Generic type placeholders allow the caller of a function to concretize the type that’s used in the generic function.
  • Protocol types allow us to return any type from a function, provided it conforms to the protocol.
  • That doesn’t work with an associated type, because concrete type information is missing.

So, we use the some keyword to create an opaque type, a reverse generic type, and let the implementation of the function determine the concrete type of the return value, and the concrete type of any associated type.

Why Are Opaque Types Useful?

Before we call it quits, let’s discuss why we need opaque types in practical Swift development.

Associated Types

Firstly, opaque types let you use protocols with associated types as return types.

As you’ve seen in the previous examples, because of the some keyword, the makeShape() function can return a value of type Shape, a protocol that uses an associated type.

Without using some here, you’d encounter that “… can only be used as a generic constraint …” error.

Type Identity

Secondly, opaque types preserve type identity, unlike protocol types.

You can compare one value returned from makeShape() with another, using ==, if you’ve used the some keyword.

Here’s an example:

protocol Shape: Equatable {

}

func makeShape() -> some Shape {
return Square(color: “Purple”)
}

let aShape = makeShape()
let anotherShape = makeShape()

print(aShape == anotherShape)
// Output: true

The Equatable protocol, that Shape conforms to, uses the == function to compare two values with each other. It’s automatically created for us, but the default function declaration looks like this:

static func == (lhs: Self, rhs: Self) -> Bool

See that Self there? It’s another placeholder that refers to the name of the current type. Just like placeholder T can refer to Int in a generic function, Self refers to a concrete type when used in the Shape protocol.

Because of that placeholder Self, Swift cannot synthesize an overload for ==, because its concrete type cannot be inferred. For all it knows, its trying to compare a Square to a Circle, and that will never work.

What do we do? As seen in the above example, we add some to the declaration of makeShape(). At compile time, Swift figures out that the concrete return type of makeShape() must be Square. Again, when we code it like this, we don’t yet know the concrete type, but at compile time, Swift knows.

SwiftUI

Thirdly – last but definitely not least – opaque types are crucial for SwiftUI.

Remember that body property, of a SwiftUI view? It uses the opaque type some View, like this:

var body: some View {
VStack(alignment: .leading) {
Text(“My hovercraft is full of eels”)
.font(.headline)
Text(“Mijn luchtkussenboot zit vol paling”)
.font(.subheadline)
}
}

The concrete type of this view, declared as some View, something like VStack>. SwiftUI uses generic structures, such as the VStack, to create awfully complex types. Compose the view differently, with more subviews, and that type only gets more complex.

You could type the body property concretely, but you would have to update this type every time the composition of the view changes. It’s easier to declare the property as some View, an opaque type, and let the implementation determine its concrete type at compile time. We, the developers, get to stay deliberately opaque – and that’s good for productivity and flexibility!

Further Reading

So much complexity in four simple characters: some. It’s starting to make sense, right? This function (or property) returns “some type”. It has a concrete type – we’re certain – but we just don’t know which one… yet!


Aasif Khan

Head of SEO at Appy Pie

App Builder

Most Popular Posts