Using inout with exitential type: is it a supported feature?

SE-0352 has the following (emphasis mine):

It's also possible to open an inout parameter. The generic function will operate on the underlying type, and can (e.g.) call mutating methods on it, but cannot change its dynamic type because it doesn't have access to the existential box:

func testOpenInOut(p: any P) {
 var mutableP: any P = p
 openInOut(&mutableP) // okay, opens to 'mutableP' and binds 'T' to its underlying type
}

I wonder what does the emphasized text mean? It seems to suggest the following code is invalid, but it works well in my experiments:

func openInOut<T: P>(_ value: inout T) { }
func testOpenInOut(p: inout any P) {
  openInOut(&p) 
}
A complete example
protocol P {
    var value: Int { get set}
}

struct S: P {
    var value: Int = 1
}

func openInOut<T: P>(_ t: inout T) {
    t.value = 2
}

func mutateInOutP(_ p: inout any P) {
    openInOut(&p)
}

func test() {
  var mutableP: any P = S()
  print("p value (before running mutateInOutP()): \(mutableP.value)")
  mutateInOutP(&mutableP)
  print("p value (after running mutateInOutP()): \(mutableP.value)")
}

test()

// output:
// p value (before running mutateInOutP()): 1
// p value (after running mutateInOutP()): 2

I ask because I had never read about such usage on the net (I did experiments in the past and IIRC it failed to compile). However, while I read SE-0352 today, I find it works. So I'd like to confirm. This is a supported feature, right? Have anyone used it?

My understanding is that that that sentence you're referring to:

It's also possible to open an inout parameter. The generic function will operate on the underlying type, and can (e.g.) call mutating methods on it, but cannot change its dynamic type because it doesn't have access to the existential box ...

is not saying you can't mutate existentials in the way you're currently doing. If you dig into that section a bit more, it's specifically describing a goal about how the ability to "open" existentials should fit into Swift's existing type system:

This section describes the details of opening an existential and then type-erasing back to an existential. These details of this change should be invisible to the user, and manifest only as the ability to use existentials with generics in places where the code would currently be rejected.

Let's take a version of the example you gave:

protocol Wearable { }

struct Shoe: Wearable { }

struct Shirt: Wearable { }

and then two functions capable of mutating Wearables:

func mutate<T: Wearable>(genericWearable: inout T) {
    // ...
}

func mutate(anyWearable: inout any Wearable) {
    // ...
}

These two functions have different abilities in terms of their ability to mutate the type of an any Wearable.

mutate(anyWearable:) can change the Wearable it receives to an entirely different type than whatever the original was, so long as it receives an existential Wearable:

func mutate(anyWearable: inout any Wearable) {
    anyWearable = Shirt()
}

var shoe: any Wearable = Shoe()

mutate(anyWearable: &shoe)

// `shoe` is now a `Shirt` :)

mutate(genericWearable:) on the other hand can't do this:

func mutate<T: Wearable>(genericWearable: inout T) {
    genericWearable = Shirt() // Cannot assign value of type 'Shirt' to type 'T'
}

var shoe: any Wearable = Shoe()

mutate(genericWearable: &shoe)

That sentence you're referring to is describing this difference, and saying "the ability to open and mutate existentials maintains the differences between these two kinds of functions", and thus introduces the ability to "open" existentials in a way that is compatible with the existing type system paradigm.

10 Likes