Swiftwater Logo

“Some” People Say…

This entry is part 19 of 21 in the series Swiftwater

ABSTRACT

Swift 5.1 (Introduced in June of 2019 at the Apple WWDC, and scheduled to “go live” in the fall of 2019) introduces what Apple calls “opaque types.”

THE BASICS

QUICK NOTE: The following code should be run using Xcode 11 (in beta, at the time of the writing of this article).

If you have started working with SwiftUI, you have already seen this in action, in the Apple tutorial projects:

struct ContentView : View {
    var body: some View {

This addresses a fairly fundamental issue with Swift Protocols, that is, they aren’t actually real types. They are contracts.

The only thing that a protocol gives you, is a promise of conformance to an interface. It guarantees nothing else, including default protocol behavior.

Default protocol behavior is different from classic object-oriented behavior; even though it looks exactly the same.

For one thing, it can’t be overridden or modified. It can only be replaced. It can be inherited –sort of. It doesn’t use the same internal logic to “cascade” inherited functionality. Instead of a vTable, which is a runtime dispatcher, it uses a direct functional reference, which is a compile-time, hardcoded director. This is safer, faster, and a lot more limiting, than standard object-oriented inheritance. When you replace an “inherited” protocol function, you are simply saying “Call this one, because the other one doesn’t exist.” Additionally, you can’t call “super” on the default protocol implementation. Your replacement is the alpha and omega of that function.

So that means, that when you use a protocol as a type indicator, it needs to be done in a way that prevents the user of the declaration from assuming anything about the type, other than what the protocol promises. It can’t even assume that AnyObject applies, as that means that the type is a class, and not a struct or enum. Protocols apply to all. You can specialize protocols, but that’s not always possible.

WHAT DOES IT ALL MEAN?

This answer explains it fairly well (I don’t think that Apple’s answer is especially helpful). I will riff off of the StackOverflow answer in my attached playground.

Basically, it forces you to tell the compiler more about the protocol’s implementation than would normally be available with a regular protocol type, which frees up some extra functionality.

If the compiler knows what the type is, it doesn’t matter whether or not the caller knows it. You can still do some type-ish things with the return, like so:

struct ContentView : View {
// Protocol P2 is a PAT (Protocol with Associated Type). Currently, Swift does not allow these protocols to be used as type placeholders.
protocol P2 {
    associatedtype PType
    var returnSomething: PType { get }
}

// We define a struct that conforms to P2, giving the type as a String
struct S3 : P2 {
    typealias PType = String
    var returnSomething: PType {
        return "One"
    }
}

// We define a struct that conforms to P2, giving the type as an Int
struct S4 : P2 {
    typealias PType = Int
    var returnSomething: PType {
        return 1
    }
}

// The below won't work. You get:
// error: Protocol 'P2' can only be used as a generic constraint because it has Self or associated type requirements
//func peeToo() -> P2 {
//    return S3()
//}

// However, if you add a "some" keyword, like so:
func peeToo() -> some P2 {
    return S3()
}
// It works.

// This also works:
func peeThree() -> some P2 {
    return S4()
}

// This won't:
//func peeFour(_ x: Int) -> some P2 {
//    return (...5).contains(x) ? S3() : S4()
//}

It’s basically making the return type-safe. Here’s another example, based on the previously declared types:

protocol P {
    func printAhh();
}

// Default implementation
extension P {
    func printAhh() {
        print("AHHH...")
    }
}

// S0 uses the default
struct S0 : P {
    func printOoh() {
        print("OOOOHHH!")
    }
}

// These replace the default with their own, and add their own functions.
struct S1 : P {
    func printAhh() {
        print("AH")
    }
    func printArgh() {
        print("ARGH...")
    }
}

//                                •

//                                •

//                                •

// Note that there's no issue with this. The function can return ANY ONE OF S0, S1, or S2.
func genericProtocolReturn(_ x: Int) -> P {
    switch x {
    case ...5:
        return S0()

    case 5...10:
        return S1()
        
    default:
        return S2()
    }
}

// If you make it a "some" response, though, you have to decide what will be returned. There can only be one (Highlander quote).
func specificProtocolReturnS1(_ x: Int) -> some P {
// Uncomment the lines below to see an error.
//    switch x {
//    case ...5:
//        return S0()
//
//    case 5...10:
//        return S1()
//
//    default:
//        return S2()
//    }

    return S1()
}

//                                •

//                                •

//                                •

// Note that this is flexible. You can assign multiple types to "genericResponse"
var genericResponse = genericProtocolReturn(1)
print(genericResponse)  // Prints "S0"
// This works:
genericResponse = specificProtocolReturnS1(1)
print(genericResponse)  // Prints "S1"
// That's because genericResponse is not "type safe." It only knows that it is a protocol of P.

// However, when we switch the order, so the first call uses the "some" function:
var genericResponse2 = specificProtocolReturnS1(1)
print(genericResponse2)  // Prints "S1"
// Now, this won't work:
// You get a "fixable" error of "Cannot assign value of type 'P' to type 'some P'"
//genericResponse2 = genericProtocolReturn(1)
// That's because genericResponse2 is now "type safe." Internally, it knows what it is (an S1).

Basically, with the “some” modifier, you are stating that “This is an instance of something <Protocol>. We’re not going to tell you what it is, but we know, and we’re keeping it a secret. If you try to use it as something other than the secret type that we know it to be, we’ll spank you at compile time.”

Adding “some” to the declaration will force the compiler to ensure that it, at least, knows what will be coming out. The caller context doesn’t need to know exactly what it will be, but it will know that it will be consistent and type-safe.

Knowing that a general function that doesn’t specify a specific type will actually be type-safe is important.

SAMPLE PLAYGROUND

protocol P {
    func printAhh();
}

// Default implementation
extension P {
    func printAhh() {
        print("AHHH...")
    }
}

// S0 uses the default
struct S0 : P {
    func printOoh() {
        print("OOOOHHH!")
    }
}

// These replace the default with their own, and add their own functions.
struct S1 : P {
    func printAhh() {
        print("AH")
    }
    func printArgh() {
        print("ARGH...")
    }
}

struct S2 : P {
    func printAhh() {
        print("WHUT?")
    }
    func printOoh() {
        print("OO")
    }
}

// Note that S2 has a "printOoh()" function, just like S0. However, these are different.
// They aren't promised by the protocol, so they can't be considered to be that instance's implementation of a protocol contract.
// They are allowed, because there's no relationship between S2 and S0, other than that specified by the P protocol.

// Note that there's no issue with this. The function can return ANY ONE OF S0, S1, or S2.
func genericProtocolReturn(_ x: Int) -> P {
    switch x {
    case ...5:
        return S0()

    case 5...10:
        return S1()
        
    default:
        return S2()
    }
}

// If you make it a "some" response, though, you have to decide what will be returned. There can only be one (Highlander quote).
func specificProtocolReturnS1(_ x: Int) -> some P {
// Uncomment the lines below to see an error.
//    switch x {
//    case ...5:
//        return S0()
//
//    case 5...10:
//        return S1()
//
//    default:
//        return S2()
//    }

    return S1()
}

func specificProtocolReturnS2(_ x: Int) -> some P {
// Uncomment the lines below to see an error.
//    switch x {
//    default:
//        return S0()
//    }

    return S2()
}

// The below won't work. You get:
// error: Protocol 'Collection' can only be used as a generic constraint because it has Self or associated type requirements
// Collection is a protocol with an associated type
//func giveMeACollection() -> Collection {
//    return [1, 2, 3]
//}

// Protocol P2 is a PAT (Protocol with Associated Type). Currently, Swift does not allow these protocols to be used as type placeholders.
protocol P2 {
    associatedtype PType
    var returnSomething: PType { get }
}

// We define a struct that conforms to P2, giving the type as a String
struct S3 : P2 {
    typealias PType = String
    var returnSomething: PType {
        return "One"
    }
}

// We define a struct that conforms to P2, giving the type as an Int
struct S4 : P2 {
    typealias PType = Int
    var returnSomething: PType {
        return 1
    }
}

// The below won't work. You get:
// error: Protocol 'P2' can only be used as a generic constraint because it has Self or associated type requirements
//func peeToo() -> P2 {
//    return S3()
//}

// However, if you add a "some" keyword, like so:
func peeToo() -> some P2 {
    return S3()
}
// It works.

// This also works:
func peeThree() -> some P2 {
    return S4()
}

// This won't:
//func peeFour(_ x: Int) -> some P2 {
//    return (...5).contains(x) ? S3() : S4()
//}

let functionResponse = peeToo()
print(functionResponse) // Prints "S3()"
let responseVal = functionResponse.returnSomething
print(responseVal)  // Prints "One"

let functionResponse2 = peeThree()
print(functionResponse2) // Prints "S4()"
let responseVal2 = functionResponse2.returnSomething
print(responseVal2)  // Prints "1"

// Note that this is flexible. You can assign multiple types to "genericResponse"
var genericResponse = genericProtocolReturn(1)
print(genericResponse)  // Prints "S0"
// This works:
genericResponse = specificProtocolReturnS1(1)
print(genericResponse)  // Prints "S1"
// That's because genericResponse is not "type safe." It only knows that it is a protocol of P.

// However, when we switch the order, so the first call uses the "some" function:
var genericResponse2 = specificProtocolReturnS1(1)
print(genericResponse2)  // Prints "S1"
// Now, this won't work:
// You get a "fixable" error of "Cannot assign value of type 'P' to type 'some P'"
//genericResponse2 = genericProtocolReturn(1)
// That's because genericResponse2 is now "type safe." Internally, it knows what it is (an S1).

// If you try "fixing" it, as the compiler suggests, then you get this error:
// 'some' types are only implemented for the declared type of properties and subscripts and the return type of functions
//genericResponse2 = genericProtocolReturn(1) as! some P
// That's because you can only use "some" in certain places, and regular variable definitions ain't where they go.

// This is not allowed.
//func foo() -> Equatable {
//    return 5
//}

// Adding "some" enables the return.
func foo() -> some Equatable {
    return 5 // The opaque result type is inferred to be Int.
}

let x = foo()
let y = foo()

print(x == y) // Legal. Both x and y have the return type of foo (which is a hidden Int). We don't care that they are Int; only that they are Equatable, and the same type.

func bar() -> some Equatable {
    return 5 // The opaque result type is again inferred to be Int.
}

let z = bar()
//print(x == z) // This is illegal. Even though both are Int, the context doesn't know that. There's not enough information to satisfy Equatable.

// In this case, the return will be inferred as a String.
func barry() -> some Equatable {
    return "5"
}

let xx = barry()
//print(x == xx) // This is illegal. Since the types are opaque, the compiler errs on the side of caution.