The "Some" Keyword In Swift

Written by Reinder de Vries on October 18 2019 in App Development

The

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 article, 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 new some keyword
  • Protocols vs. opaque types, and associated types

Ready? Let’s go.

  1. It’s “some” Thing ¯\_(ツ)_/¯
  2. Opaque Types & Generics
  3. Protocol Types & Associated Types
  4. Wrapping Up: Opaque Types
  5. Why Opaque Types Are Useful
  6. Further Reading

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

A new feature in Swift 5.1 is the some keyword. You might have first seen it as part of the default SwiftUI View template, 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 article. Let’s dive in!

Become a professional  iOS developer

Get started with iOS 13 and Swift 5

Sign up for my iOS development course to learn iOS development with Swift 5, and start your professional iOS career.

Opaque Types & Generics

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

  • With a generic type placeholder, the caller of the function determines the concrete type of a placeholder.
  • With opaque types, the implementation determines the concrete type.

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.

Take a look at this example:

func addition<T: Numeric>(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, etcetera.

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

  • 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.
  • 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.

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.

  • 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 & 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.

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 article 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.

They’ve also given the color property a concrete type. The Square uses a string for color, and the Circle uses an integer for a color. It’s simplest 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.

Wrapping Up: Opaque Types

What’s the concrete type of Color, the associated type of the Shape protocol?

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.

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.

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. Based on the implementation, i.e. the code inside the function, which uses Square, the concrete type for some Shape is Square. And 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.

  • 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 Opaque Types Are Useful

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

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.

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.

Thirdly – last but definitely not least – opaque types are crucial for SwiftUI. Remember that body property? 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, is VStack<TupleView<(Text, Text)>>. 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 (in this case).

Neat!

Become a professional  iOS developer

Get started with iOS 13 and Swift 5

Sign up for my iOS development course to learn iOS development with Swift 5, and start your professional iOS career.

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!

Want to learn more? Check out these resources:

Reinder de Vries

Reinder de Vries

Reinder de Vries is a professional iOS developer. He teaches app developers how to build their own apps at LearnAppMaking.com. Since 2009 he has developed a few dozen apps for iOS, worked for global brands and lead development at several startups. When he’s not coding, he enjoys strong espresso and traveling.