some View
is an opaque result type as introduced by SE-0244 and is available in Swift 5.1 with Xcode 11. You can think of this as being a "reverse" generic placeholder.
Unlike a regular generic placeholder which is satisfied by the caller:
protocol P {}
struct S1 : P {}
struct S2 : P {}
func foo<T : P>(_ x: T) {}
foo(S1()) // Caller chooses T == S1.
foo(S2()) // Caller chooses T == S2.
An opaque result type is an implicit generic placeholder satisfied by the implementation, so you can think of this:
func bar() -> some P {
return S1() // Implementation chooses S1 for the opaque result.
}
as looking like this:
func bar() -> <Output : P> Output {
return S1() // Implementation chooses Output == S1.
}
In fact, the eventual goal with this feature is to allow reverse generics in this more explicit form, which would also let you add constraints, e.g -> <T : Collection> T where T.Element == Int
. See this post for more info.
The main thing to take away from this is that a function returning some P
is one that returns a value of a specific single concrete type that conforms to P
. Attempting to return different conforming types within the function yields a compiler error:
// error: Function declares an opaque return type, but the return
// statements in its body do not have matching underlying types.
func bar(_ x: Int) -> some P {
if x > 10 {
return S1()
} else {
return S2()
}
}
As the implicit generic placeholder cannot be satisfied by multiple types.
This is in contrast to a function returning P
, which can be used to represent both S1
and S2
because it represents an arbitrary P
conforming value:
func baz(_ x: Int) -> P {
if x > 10 {
return S1()
} else {
return S2()
}
}
Okay, so what benefits do opaque result types -> some P
have over protocol return types -> P
?
1. Opaque result types can be used with PATs
A major current limitation of protocols is that PATs (protocols with associated types) cannot be used as actual types. Although this is a restriction that will likely be lifted in a future version of the language, because opaque result types are effectively just generic placeholders, they can be used with PATs today.
This means you can do things like:
func giveMeACollection() -> some Collection {
return [1, 2, 3]
}
let collection = giveMeACollection()
print(collection.count) // 3
2. Opaque result types have identity
Because opaque result types enforce a single concrete type is returned, the compiler knows that two calls to the same function must return two values of the same type.
This means you can do things like:
// foo() -> <Output : Equatable> Output {
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.
This is legal because the compiler knows that both x
and y
have the same concrete type. This is an important requirement for ==
, where both parameters of type Self
.
protocol Equatable {
static func == (lhs: Self, rhs: Self) -> Bool
}
This means that it expects two values that are both the same type as the concrete conforming type. Even if Equatable
were usable as a type, you wouldn't be able to compare two arbitrary Equatable
conforming values with each other, for example:
func foo(_ x: Int) -> Equatable { // Assume this is legal.
if x > 10 {
return 0
} else {
return "hello world"
}
}
let x = foo(20)
let y = foo(5)
print(x == y) // Illegal.
As the compiler cannot prove that two arbitrary Equatable
values have the same underlying concrete type.
In a similar manner, if we introduced another opaque type returning function:
// foo() -> <Output1 : Equatable> Output1 {
func foo() -> some Equatable {
return 5 // The opaque result type is inferred to be Int.
}
// bar() -> <Output2 : Equatable> Output2 {
func bar() -> some Equatable {
return "" // The opaque result type is inferred to be String.
}
let x = foo()
let y = bar()
print(x == y) // Illegal, the return type of foo != return type of bar.
The example becomes illegal because although both foo
and bar
return some Equatable
, their "reverse" generic placeholders Output1
and Output2
could be satisfied by different types.
3. Opaque result types compose with generic placeholders
Unlike regular protocol-typed values, opaque result types compose well with regular generic placeholders, for example:
protocol P {
var i: Int { get }
}
struct S : P {
var i: Int
}
func makeP() -> some P { // Opaque result type inferred to be S.
return S(i: .random(in: 0 ..< 10))
}
func bar<T : P>(_ x: T, _ y: T) -> T {
return x.i < y.i ? x : y
}
let p1 = makeP()
let p2 = makeP()
print(bar(p1, p2)) // Legal, T is inferred to be the return type of makeP.
This wouldn't have worked if makeP
had just returned P
, as two P
values may have different underlying concrete types, for example:
struct T : P {
var i: Int
}
func makeP() -> P {
if .random() { // 50:50 chance of picking each branch.
return S(i: 0)
} else {
return T(i: 1)
}
}
let p1 = makeP()
let p2 = makeP()
print(bar(p1, p2)) // Illegal.
Why use an opaque result type over the concrete type?
At this point you may be thinking to yourself, why not just write the code as:
func makeP() -> S {
return S(i: 0)
}
Well, the use of an opaque result type allows you to make the type S
an implementation detail by exposing only the interface provided by P
, giving you flexibility of changing the concrete type later down the line without breaking any code that depends on the function.
For example, you could replace:
func makeP() -> some P {
return S(i: 0)
}
with:
func makeP() -> some P {
return T(i: 1)
}
without breaking any code that calls makeP()
.
See the Opaque Types section of the language guide and the Swift evolution proposal for further information on this feature.