The magic of some and any keywords in Swift

ยท

8 min read

The magic of some and any keywords in Swift

Along with the introduction of Swift 5.7, we have been frequently witnessing the wide usage of some and any keywords in Swift code, haven't we? This is because both keywords play a major role in the Swift ecosystem helping us diminish the difficulties of dealing with protocols.

In this article, you will learn some and any keywords along with sample code, understand the difference between them and have a bigger picture of how to use them in the right places. So let's dive into it.

Understanding some keyword

With the release of Swift 5.1, the some keyword was introduced in the Swift ecosystem. It can be put in front of the type at the type declaration to convert a particular type to an opaque type without losing the originality of the type. In other words, it only belongs to a specific type and does not allow any changes of the underlying concrete type from one to another forcing it to be consistent throughout the lifetime of variables. To get a bigger picture, let's take a look at the following code snippet:

protocol Animal {
    var name: String { get set }
    func makeSound()
}

struct Dog: Animal {
    var name: String = "Dog"
    func makeSound() {
        print("Bark!")
    }
}

var animal: some Animal = Dog() // Create an opaque type of `Animal` with the underlying type of `Dog`

You will see Animal protocol and Dog struct conforming to Animal and there is also the declaration of an animal variable using some keyword. Here it creates an opaque type of Animal revealing the concrete type of Dog to the compiler so that the compiler won't allow type changes dynamically and it will yell the error when we attempt it.

struct Cat: Animal {
    var name: String = "Cat"
    func makeSound() {
        print("Meow!")
    }
}

animal = Cat() // Cannot assign value of type 'Cat' to type 'some Animal'
animal = Dog() // Cannot assign value of type 'Cat' to type 'some Animal'

If we compile the above code, we will see the error pop-ups and also notice that it prevents us to assign a new instance to the variable because the opaque type only allows a specific concrete type throughout the lifetime of a particular variable. If you are familiar with SwiftUI, you will notice that we have to return a specific type of view in the view block.

func createHeader() -> some View { // Function declares an opaque return type 'some View', but the return statements in its body do not have matching underlying types
    if isCompact {
        return VStack { // return VStack
            Text("Hi ๐Ÿ‘‹,")
            Text("Welcome to my blog")
        }
    } else {
        return Text("Hi ๐Ÿ‘‹, Welcome to my blog") // return Text
    }
}

The above code will not compile because the method tries to return different types - VStack and Text while it only accepts a specific type of view. At the moment, you may get the idea of what some keyword is. So let's move to any keyword.

Understanding any keyword

The any keyword was introduced in the Swift 5.7 release to provide more flexibility when dealing with protocols. Although the intention of the any keyword is the same with the some keyword, the way they work is quite different from each other. As it is named any, the any keyword serves as a type-eraser just like Any and AnyObject. Technically speaking, it defines the existential type which belongs to any kind of type meeting a specific protocol. Let's jump into code with our beloved animals:

var animal: any Animal = Cat()
animal = Dog() // Now the `animal` variable will hold the instance of `Dog` as `any Animal`

In the above code, we can see that with any keyword, the variable can hold any instances of types that conform to a specific protocol. That's why the compiler allows us to assign the variable with different values as it already erases the concrete type.

Comparison between some and any keywords

As you understand how the magic of some and any keywords work, let's make them compare to highlight the rules of both keywords.

As we all know that the some keyword only holds a specific type conforming to a particular protocol while the any keyword accepts various types that meet a particular protocol. We can illustrate this using the array of an opaque type - some and an existential type - any so that we will see that the compiler prompts the error when we attempt to use various types in the array of an opaque type but it will be compiled properly with the array of an existential type.


// The array of any Animal containing different types of animals
var animals: [any Animal] = [Dog(), Cat(), Dog(name: "Husky")] // Compile properly

// The array of some Animal with a specific type of animal
var animals: [some Animal] = [Dog(), Dog()] // Compile properly

// This won't allow to put different types of animals in the array
var animals: [some Animal] = [Cat(), Dog()] // Type of expression is ambiguous without more context

Another interesting thing is that we cannot append the array of an opaque type with the new element regardless of the type while the existential array can achieve it.

var animals: [any Animal] = [Dog(), Cat(), Dog(name: "Husky")]
animals.append(Cat()) // Compile properly

var animals: [some Animal] = [Dog(), Dog()]
animals.append(Dog()) // Fail to compile
animals.append(Cat()) // Fail to compile

Another thing to highlight in this section is that the return type with some and any keyword. As I mention in the earlier section, we suffer a lot when we try to return different types of views based on a specific condition in SwiftUI because of an opaque keyword. So, we have to return a specific type by force before the arrival of @ViewBuilder attribute (we won't cover it here as it is a different topic). On the other hand, we can return whatever we want using any keyword.

// Return any Animal (Dog or Cat) based on a condition
func getAnimal(_ isLoyalPet: Bool) -> any Animal { // Compile properly
    isLoyalPet ? Dog() : Cat()
}

// This won't allow to return one of different types
func getAnimal(_ isLoyalPet: Bool) -> some Animal { // Fail to compile as the return types mismatch each other
    isLoyalPet ? Dog() : Cat()
}

// It only allows us to return the same type
func getDog(_ isLoyalPet: Bool) -> some Animal {
    isLoyalPet ? Dog(name: "German Shepherd") : Dog()
}

The above code shows the limitation of using the some keyword which only allows the same type as a return type.

Utilization of some and any keywords

Along with the Swift evolution, these two keywords are extremely powerful and widely adapted in the Swift ecosystem. The usage of the some keyword grows as the introduction of SwiftUI to detach the concrete type of the associated type of View protocol so that we don't need to specify the concrete type.

struct HeaderView: View {
    var body: some View { // Return an opaque type of view instead of Text
        Text("Hi ๐Ÿ‘‹, Welcome to my blog")
    }
}

Besides, we can see these keywords in the replacement of the generic method instead of using the traditional generic approach. I also feel that it improves the readability of code because of its simple syntax.

// Using the traditional generic type
func makeAnimalSing<T: Animal>(_ animal: T) {
    animal.makeSound()
}

// Using the opaque type
func makeAnimalSing(_ animal: some Animal) {
    animal.makeSound()
}

// Using the existential type
func makeAnimalSing(_ animal: any Animal) {
    animal.makeSound()
}

All of those methods will compile properly and works functionally but the second and third methods look more readable. If we apply these keywords in the parameter, it is not obvious to see the limitation. In case these are applied to the return type, we need to consider carefully which one is the best fit for our requirements.

Another advantage is that as these keywords help us to conceal the concrete type, they are super powerful to pass dependencies through API interfaces without revealing the concrete type which is not necessary for API. Let's take a look at the snippet below:

protocol NetworkWorker {
    func fetch()
}

struct UatNetworkWorker: NetworkWorker { // network worker for uat target
    func fetch() {
        print("Fetching data on UAT server")
    }
}

struct ProdNetworkWorker: NetworkWorker { // network worker for production target
    func fetch() {
        print("Fetching data on PROD server")
    }
}

class APIService {
    let networkWorker: any NetworkWorker
    init(_ networkWorker: some NetworkWorker) {
        self.networkWorker = networkWorker
    }

    func load() {
        networkWorker.fetch()
    }
}

In the above code, we have APIService depending on the instance of NetworkWorker to perform networking based on the target. That's why we only inject the blueprint by hiding the concrete type and APISerive have no idea of the actual type of network worker.

let networkWorker: any NetworkWorker
#if UAT
networkWorker = UatNetworkWorker()
#else
networkWorker = ProdNetworkWorker()
#endif
let apiService = APIService(networkWorker)
apiService.load()

So we can manipulate the network worker based on the target and inject it accordingly because of the power of some and any keywords.

Drawback of some and any keywords

As these keywords hide the concrete type of the instances, sometimes it struggles to know its concrete type in case we want to access it. If we want to reveal it, we have to manually typecast the instance and handle the error unless the types match each other.

Conclusion

To conclude, the some and any keywords have become powerful and magical keywords in the Swift ecosystem and it is critical to understand and master these two keywords by applying them in our code on our daily basis. The more we applied new techniques we learned, the stronger our experience and competence in our domain. So let's apply them in code from today.

ย