Developers who have worked with Apple’s SwiftUI framework have likely encountered the some keyword in the context of defining custom views, where it’s typically used to enable the generic View protocol to be referenced as a type when declaring a view’s body:
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
.foregroundStyle(.blue)
.font(.headline)
}
}When using the some keyword, the compiler will infer the underlying concrete type that’s actually being returned, which is especially helpful when working with APIs that heavily utilize generics — such as SwiftUI. Without some, we’d have to always manually specify the concrete type of our entire view hierarchy, which even in the case of the above (very simple) ContentView is as complex as this:
ModifiedContent<ModifiedContent<ModifiedContent<Text,
_PaddingLayout>, _ForegroundStyleModifier<Color>>,
_EnvironmentKeyWritingModifier<Optional<Font>>>But the some keyword isn’t just useful when working with SwiftUI. In fact, we can use it to solve similar generics-based problems, and as syntactic sugar, across a wide range of use cases.
Generic function parameters
Let’s say that we’re working on a video watching app, and that our app’s Database type contains an API for saving a set of videos to the user’s “watch later” list:
actor Database {
...
func saveToWatchLater(_ videos: [Video]) {
var watchLater = loadWatchLater()
for video in videos {
watchLater.addVideo(withID: video.id)
}
save(watchLater)
}
}Currently, our API only accepts arrays of Video values, but across our app, videos might be stored using different kinds of collections — including sets, dictionaries, and perhaps custom collections/sequences as well. So, to make our saveToWatchLater API a bit more generic we could, well, actually make it generic:
actor Database {
...
func saveToWatchLater<T: Sequence>(_ videos: T) where T.Element == Video {
...
}
}That works, but it does arguably add a bit of complexity to our function declaration. This is where the some keyword comes in, which — just like when using SwiftUI — can help us avoid some of the complexity involved in working with generics. In this case, we can use it to be able to reference the Sequence protocol directly, which works since that protocol marks its associated Element type as its primary associated type.
actor Database {
...
func saveToWatchLater(_ videos: some Sequence<Video>) {
...
}
}With that, we’ve ended up with a quite simple solution that still lets us leverage the power and flexibility that generics offer, all without introducing any kind of type erasure or runtime overhead.
An alternative to type erasure or boxing
Speaking of type erasure, the some keyword can also help us avoid having to perform type erasure, or otherwise box a given value or object into something that needs to be resolved at runtime (which tends to add both complexity and overhead).
For example, let’s say that we’re structuring various async operations across our app using an Operation protocol, which in turn emits OperationEvent values when performed — sort of like a lightweight, less complex version of something like a Combine publisher:
enum OperationEvent<Output> {
case progressUpdate(completionRatio: Double)
case finished(Output)
case failed(Error)
}
protocol Operation<Output> {
associatedtype Output
typealias EventStream = AsyncStream<OperationEvent<Output>>
func perform() -> EventStream
}Our
Operationprotocol declares itsOutputtype as its primary associated type, using the same syntax as concrete types use when declaring their generic type lists.
One way of using the above protocol, for example in order to implement upload and download networking operations, would be to have our various APIs return concrete, Operation-conforming types that’ll become known to each call site:
actor NetworkService {
...
func downloadOperation(for url: URL) -> DownloadOperation {
DownloadOperation(url: url, service: self)
}
func uploadOperation(for upload: Upload) -> UploadOperation {
UploadOperation(upload: upload, service: self)
}
}The above works, but it does arguably expose a bit too many implementation details to the caller. For example, if the above NetworkService would be part of a Swift Package’s public API, then we’d have to also make all return types (such as DownloadOperation and UploadOperation) public as well, which we might not want to do.
Ideally, we might want to make all operation implementations private, which would give our NetworkService a much simpler and clearer API design, for example using a setup like this:
private extension NetworkService {
struct DownloadOperation: Operation<Data> {
var url: URL
var service: NetworkService
func perform() -> EventStream {
...
}
}
struct UploadOperation: Operation<Void> {
var upload: Upload
var service: NetworkService
func perform() -> EventStream {
...
}
}
}However, the above change does present us with a problem — what are we supposed to return from our various operation-building functions? This is a case where, historically, type erasure has often been used, to enable us to return an opaque wrapper that boxes our underlying implementation’s actual logic. In this case, we could make such a wrapper capture the underlying type’s perform method as a closure:
struct AnyOperation<Output>: Operation<Output> {
var performer: () -> EventStream
func perform() -> EventStream {
performer()
}
}
actor NetworkService {
...
func downloadOperation(for url: URL) -> AnyOperation<Data> {
let operation = DownloadOperation(url: url, service: self)
return AnyOperation(performer: operation.perform)
}
func uploadOperation(for upload: Upload) -> AnyOperation<Void> {
let operation = UploadOperation(upload: upload, service: self)
return AnyOperation(performer: operation.perform)
}
}This is another case where the some keyword can really help us simplify things. Instead of having to manually do type erasure, we can achieve the same kind of separation of concerns and clean API design simply by having each of our operation-building functions return some Operation, specialized with the type of value that each operation produces — like this:
actor NetworkService {
...
func downloadOperation(for url: URL) -> some Operation<Data> {
DownloadOperation(url: url, service: self)
}
func uploadOperation(for upload: Upload) -> some Operation<Void> {
UploadOperation(upload: upload, service: self)
}
}Much simpler, and again, without introducing additional runtime overhead, since types annotated with the some keyword are always resolved at compile time. That’s one of the key differences between this solution compared to if we instead would’ve chosen to use the any keyword, which also lets us refer to a generic protocol as a type, but with the important difference that it boxes the underlying value into a so-called existential, which needs to be unboxed and resolved at runtime.
That of course doesn’t mean that the any keyword is just a worse version of some — existentials have their use cases too, which we’ll take a closer look at in future articles.
Conclusion
When certain Swift features are prominently used within a specific framework or domain, it can be easy to think of such a feature as just a tool for that specific task — as is often the case with the some keyword and SwiftUI. But hopefully this article has illustrated that some is an incredibly useful feature that can be used to reduce complexity when working with all sorts of generics, and to create very neat APIs that cleanly abstract their implementation details from their call sites. All without sacrificing performance or introducing additional runtime logic.
Thanks for reading!
