REALLY Understanding Swift ‘some’ keyword (and Swift Opaque types, consequently)

TLDR;

It is like compiler substitution :)

HTTR;

To understand associatedtypes: https://02infinity.medium.com/understanding-swift-protocol-associated-types-ca717d091b56

I’ve had a hard time wrapping my head around ‘some’ keyword (introduced in swift 5.1). Now that I have that “Ah-ha!” moment, I thought I may be able to help others understand, with this article.

I’ll start with a simple example.

Say you want two different types of numbers, and want to print it:

struct IntegerNumber {
var value: Int
func printValue() {
print(“value: \(value)”)
}
}
struct FloatingPointNumber {
var value: Float
func printValue() {
print(“value: \(value)”)
}
}

Since both have a common function of printing the value it represents, we can abstract the functionality to a protocol, leveraging the power of associatedtypes (generics for protocols), and make our number types conform to it like so:

protocol Number {
associatedtype T
var value: T { get } func printValue()
}
struct IntegerNumber: Number {
var value: Int
func printValue() {
print(“value is Integer \(value)”)
}
}
struct FloatingPointNumber: Number {
var value: Float
func printValue() {
print(“value is Floating point \(value)”)
}
}

Suppose we want to select just one of them and print it.

What shall we do? Create a variable of type Number protocol????

// ERROR: protocol ‘Number’ can only be used as a generic constraint
// because it has Self or associated type requirements !!!!
var selectedNumber: Number = integerNumber //???????? CANNOT!!!!!

Compile error! Because our protocol Number has an associatedtype. Why so id a discussion fo another time/article.

For this to work, we will have to leverage Swift’s opaque types.

Opaque types are types that the compiler substitutes for us, based on what we use on the right side of the “=” symbol (RHS).

It tells the user that it is of a particular type (say a protocol or a class), but unlike making use of the protocol type, this type has to be known at compile time so that the compiler can make the necessary substition.

Here we will have to use

var selectedNumber: some Number = integerNumber // 1
// or
var selectedNumber: some Number = floatingNumber // 2

The compiler substitues the “some Number” with “IntegerNumber” for case 1, and FloatingPointNumber for case 2:

var selectedNumber: IntegerNumber = integerNumber // 1
// or
var selectedNumber: FloatingPointNumber = floatingNumber // 2

Now we can print the number using the conformed protocols method like:

selectedNumber.printValue()

Since the compiler hard sets (strongly types) the selectedNumber to either IntegerNumber or FloatingPointNumber on the first assignment, we cannot reassign it to the other variable like:

var selectedNumber: some Number = integerNumber//ERROR! the compiler substituted ‘some Number’ with type IntegerNumber!
selectedNumber = floatingNumber

Likewise, if you assign floatingNumber to selectedNumber, you cannot assign integerNumber to it afterwards.

var selectedNumber: sonme Number = floatingNumber// ERROR! the compiler substituted ‘some Number’ with type IntegerNumber!
selectedNumber = integerNumber

The Type of selectedNumber is the Type you assign it to first!

To confirm that this is a compiler level substitution, I tried to create a function to test this out. This function returns an Opaque type, in whose implementation I tried to return objects of classes that conform to Number protocol:

//ERROR! Function declares an opaque return type, but the return statements in its body do not have matching underlying typesfunc createNumber() -> some Number {
let num = arc4random() % 10
if num % 2 == 0 {
return IntegerNumber(value: 11)
} else {
return FloatingPointNumber(value: Float(num))
}
}

In the above, the compiler sees two different CONCRETE types being returned, and will complain.

Now it works if I return same type of objects in the return statements, thus confirming this is a compiler substitution:

// THIS compiles
func createNumber() -> some Number {
let num = arc4random() % 10
if num % 2 == 0 {
return IntegerNumber(value: 11)
} else {
return IntegerNumber(value: Int(num))
}
}

In C terms, this looks to be like pre-processor macro expansion!

This is also not very unlike C++’s lovely constexpr (when the conditions are positive)!

Hope this explanation and investigation helps you understand the ‘some’ and opaque types.