What is the “some” keyword in Swift?
Published on: December 6, 2019If you have spent some time with SwiftUI or if you have watched the WWDC videos on SwiftUI this year, you may have noticed that views in SwiftUI have a property called body
of type some View
. The some
keyword is new in Swift 5.1 and it’s part of a feature called opaque result types (SE-0244). What is this some
keyword then? And how can you use it in your code?
I aim to answer these questions in this blog post. We’ll first explore what opaque result types are, and more specifically what problem they solve. Next, we’ll look at how opaque result types are used in SwiftUI and we’ll discover whether it’s a Swift feature that you’re likely to adopt in your code at some point.
Exploring opaque result types
To fully understand the problems solved by opaque result types, it’s good to have a solid understanding of generics. If you’re not familiar with generics at all, I recommend reading these to posts I wrote to get yourself up to speed:
- An introduction to generics in Swift using its built-in types
- Building flexible components with generics and protocols
If you’re not interested in learning loads about generics and just want to learn about opaque result types and what the some
keyword is, that’s fine too. Just be aware that some of the content in this post could be confusing without understanding generics.
In Swift, we can use protocols to define interfaces or contracts for our objects. When something conforms to a protocol, we know that it can do certain things, or has certain properties. This means that you can write code like this:
protocol ListItemDisplayable {
var name: String { get }
}
struct Shoe: ListItemDisplayable {
let name: String
}
var listItem: ListItemDisplayable = Shoe(name: "a shoe")
When using this listItem
property, only the properties exposed by ListItemDisplayable
are exposed to us. This is especially useful when you want to have an array of items that are ListItemDisplayable
where the concrete types can be more than just Shoe
:
struct Shoe: ListItemDisplayable {
let name: String
}
struct Shorts: ListItemDisplayable {
let name: String
}
var mixedList: [ListItemDisplayable] = [Shoe(name: "a shoe"),
Shorts(name: "a pair of shorts")]
The compiler treats our Shoe
and Shorts
objects as ListItemDisplayable
, so users of this list won't know whether they’re dealing with shoes, shorts, jeans or anything else. All they know is that whatever is in the array can be displayed in a list because it conforms to ListDisplayable
.
Opaque result types for protocols with associated types
The flexibility shown in the previous section is really cool, but we can push our code further:
protocol ListDataSource {
associatedtype ListItem: ListItemDisplayable
var items: [ListItem] { get }
var numberOfItems: Int { get }
func itemAt(_ index: Int) -> ListItem
}
The above defines a ListDataSource
that holds some list of an item that conforms to ListItemDisplayable
. We can use objects that conform to this protocol as data source objects for table views, or collection views which is pretty neat.
We can define a view model generator object that will, depending on what kind of items we pass it, generate a ListDataSource
:
struct ShoesDataSource: ListDataSource {
let items: [Shoe]
var numberOfItems: Int { items.count }
func itemAt(_ index: Int) -> Shoe {
return items[index]
}
}
struct ViewModelGenerator {
func listProvider(for items: [Shoe]) -> ListDataSource {
return ShoesDataSource(items: items)
}
}
However, this code doesn’t compile because ListDataSource
is a protocol with associated type constraints. We could fix this by specifying ShoesDataSource
as the return type instead of ListDataSource
, but this would expose an implementation detail that we want to hide from users of the ViewModelGenerator
. Callers of listProvider(for:)
only really need to know is that we’re going to return a ListDataSource
from this method. We can rewrite the generator as follows to make our code compile:
struct ViewModelGenerator {
func listProvider(for items: [Shoe]) -> some ListDataSource {
return ShoesDataSource(items: items)
}
}
By using the some
keyword, the compiler can enforce a couple of things while hiding them from the caller of listProvider(for:)
:
- We return something that conforms to
ListDataSource
. - The returned object’s associated type matches any requirements that are set by
ListDataSource
. - We always return the same type from
listProvider(for:)
.
Especially this last point is interesting. In Swift, we rely on the compiler to do a lot of compile-time type checks to help us write safe and consistent code. And in turn, the compiler uses all of this information about types to optimize our code to ensure it runs as fast as possible. Protocols are often a problem for the compiler because they imply a certain dynamism that makes it hard for the compiler to make certain optimizations at compile time which means that we’ll take a (very small) performance hit at runtime because the runtime will need to do some type checking to make sure that what’s happening is valid.
Because the Swift compiler can enforce the things listed above, it can make the same optimizations that it can when we would use concrete types, yet we have the power of hiding the concrete type from the caller of a function or property that returns an opaque type.
Opaque result types and Self requirements
Because the compiler can enforce type constraints compile time, we can do other interesting things. For example, we can compare items that are returned as opaque types while we cannot do the same with protocols. Let’s look at a simple example:
protocol ListItemDisplayable: Equatable {
var name: String { get }
}
func createAnItem() -> ListItemDisplayable {
return Shoe(name: "a comparable shoe: \(UUID().uuidString)")
}
The above doesn’t compile because Equatable
has a Self
requirement. It wants to compare two instances of Self
where both instances are of the same type. This means that we can’t use ListItemDisplayable
as a regular return type, because a protocol on its own has no type information. We need the some
keyword here so the compiler will figure out and enforce a type for ListItemDisplayable
when we call createAnItem()
:
func createAnItem() -> some ListItemDisplayable {
return Shoe(name: "a comparable shoe: \(UUID().uuidString)")
}
The compiler can now determine that we’ll always return Shoe
from this function, which means that it knows what Self
for the item that’s returned by createAnItem()
, which means that the item can be considered Equatable
. This means that the following code can now be used to create two items and compare them:
let left = createAnItem()
let right = createAnItem()
print(left == right)
What’s really cool here is that both left
and right
hide all of their type information. If you call createAnItem()
, all you know is that you get a list item back. And that you can compare that list item to other list items returned by the same function.
Opaque return types as reverse generics
The Swift documentation on opaque result types sometimes refers to them as reverse generics which is a pretty good description. Before opaque result types, the only way to use protocols with associated types as a return type would have been to place the protocol on a generic constraint for that method. The downside here is that the caller of the method gets to decide the type that’s returned by a function rather than letting the function itself decide:
protocol ListDataSource {
associatedtype ListItem: ListItemDisplayable
var items: [ListItem] { get }ƒ
var numberOfItems: Int { get }
func itemAt(_ index: Int) -> ListItem
init(items: [ListItem])
}
func createViewModel<T: ListDataSource>(for list: [T.ListItem]) -> T {
return T.init(items: list)
}
func createOpaqueViewModel<T: ListItemDisplayable>(for list: [T]) -> some ListDataSource {
return GenericViewModel<T>(items: list)
}
let shoes: GenericViewModel<Shoe> = createViewModel(for: shoeList)
let opaqueShoes = createOpaqueViewModel(for: shoeList)
Both methods in the preceding code return the exact same GenericViewModel
. The main difference here is that in the first case, the caller decides that it wants to have a GenericViewModel<Shoe>
for its list of shoes, and it will get a concrete type back of type GenericViewModel<Shoe>
. In the example that uses some
, the caller only knows that it will get some ListDataSource
that holds its list of ListItemDisplayable
items. This means that the implementation of createOpaqueViewModel
can now decide what it wants to do. In this case, we chose to return a generic view model. We could also have chosen to return a different kind of view model instead, all that matters is that we always return the same type from within the function body and that the returned object conforms to ListDataSource
.
Using opaque return types in your projects
While I was studying opaque return types and trying to come up with examples for this post, I noticed that it’s not really easy to come up with reasons to use opaque return types in common projects. In SwiftUI they serve a key role, which might make you believe that opaque return types are going to be commonplace in a lot of projects at some point.
Personally, I don’t think this will be the case. Opaque return types are a solution to a very specific problem in a domain that most of us don’t work on. If you’re building frameworks or highly reusable code that should work across many projects and codebases, opaque result types will interest you. You’ll likely want to write flexible code based on protocols with associated types where you, as the builder of the framework, have full control of the concrete types that are returned without exposing any generics to your callers.
Another consideration for opaque return types might be their runtime performance. As discussed earlier, protocols sometimes force the compiler to defer certain checks and lookups until runtime which comes with a performance penalty. Opaque return types can help the compiler make compile-time optimizations which is really cool, but I’m confident that it won’t matter much for most applications. Unless you’re writing code that really has to be optimized to its core, I don’t think the runtime performance penalty is significant enough to throw opaque result types at your codebase. Unless, of course, it makes a lot of sense to you. Or if you’re certain that in your case the performance benefits are worth it.
What I’m really trying to say here is that protocols as return types aren’t suddenly horrible for performance. In fact, they sometimes are the only way to achieve the level of flexibility you need. For example, if you need to return more than one concrete type from your function, depending on certain parameters. You can’t do that with opaque return types.
This brings me to quite possibly the least interesting yet easiest way to start using opaque return types in your code. If you have places in your code where you’ve specified a protocol as return type, but you know that you’re only returning one kind of concrete type from that function, it makes sense to use an opaque return type instead. In fact, the Swift team is considering inferring some
whenever you use a protocol as a type in Swift 6.0. This might never make it into Swift 6.0, but it does show that the Swift team is serious about some
being a good default to try whenever you can.
A more interesting consideration to make for using some
is in places where you've defined a single use generic. For example, in the following situation you might be able to use some
instead of a generic:
class MusicPlayer {
func play<Playlist: Collection<Track>>(_ playlist: Playlist) { /* ... */ }
}
In this example, our play
function has a generic argument Playlist
that's constrained to a Collection
that holds Track
objects. We can write this constraint thanks to Swift 5.7's primary associated types. Learn more about primary associated types in this post. If we only use the Playlist
generic in a single place like a function argument, we can use some
instead of the generic from Swift 5.7 onward. Swift 5.7 allows us to use some
for function arguments which is a huge improvement.
Rewriting the example above with some
looks as follows:
class MusicPlayer {
func play(_ playlist: some Collection<Track>) { /* ... */ }
}
Much better, right?
Verify your existential usage for Swift 6 with Xcode 15.3
If you want to make sure that your app is ready for Swift 6.0 and uses any
or some
everywhere you're supposed to, pass the -enable-upcoming-feature ExistentialAny
in your Swift build flags. To learn how, take a look at this post where I dig into experimental Swift versions and features. Note that the EsistentialAny
build flag is available in the default Xcode 15.3 toolchain.
In summary
In this post you saw what problems opaque return types solve, and how they can be used by showing you several examples. You learned that opaque return types can act as a return type if you want to return an object that conforms to a protocol with associated type constraints. This works because the compiler performs several checks at compile time to figure out what the real types of a protocol’s associated types are. You also saw that opaque return types help resolve so-called Self
requirements for similar reasons. Next, you saw how opaque result types act as reverse generics in certain cases, which allows the implementer of a method to determine a return type that conforms to a protocol rather than letting the caller of the method decide.
Next, I gave some insights into what opaque result types are likely going to in your apps. With Swift 5.7's ability to use some
in more places than just return types I think some
will become a very useful tool that will help us use conrete types instead of existentials (protocols) in lots of places which should make our code more performant and robust.
If you have any questions, feedback or if you have awesome applications of opaque return types that I haven’t covered in this post, I would love to hear from you on Twitter.