Is the 'existential' a value or ref type?

In the given example the compiler complains and want that the method is marked as mutating and the property itself is a var:

protocol P {
  var array: [Int] { get set }
}

class A: P {
  var array: [Int] = []
}

struct S {
  let value: AnyObject & P

  func append() {
    value.array.append(42)
  }
}

My question is now if the "existential" box that the compiler generates for us (also known as any AnyObject & P from this thread) is itself a value or a ref type in Swift?

Value/Ref type is not really the important point here. What is important is that from the protocol point of view, a mutation can replace self (even with reference types).

Sorry if the sample code below is contrived, but it illustrates the point. Here A is a reference type, and its implementation of the P.array setter mutates self:

protocol P {
    var array: [Int] { get set }
}

protocol Initable { init() }
extension P where Self: Initable {
    var array: [Int] {
        get { return [] }
        set { self = Self.init() }
    }
}

class A: P, Initable {
    required init() { }
}

struct S {
    let value: AnyObject & P
    
    func append() {
        value.array.append(42) // Indeed this won't do
    }
}
1 Like

You can explicitly declare a refinement protocol that requires the array setter to be non-mutating:

protocol P {
    var array: [Int] { get set }
}

protocol NonMutatingP: P {
    var array: [Int] { get nonmutating set }
}

class A: NonMutatingP {
    var array: [Int] = []
}

struct S {
    let value: NonMutatingP
    
    func append() {
        value.array.append(42)
    }
}


let s = S(value: A())
s.append()
s.value.array        // [42]
(s.value as P).array // [42]

Again you see that AnyObject is not needed to make it work.

3 Likes

I didn't know about such solution before, that's a cool trick. My alternative solution would have looked like this:

protocol P {
  var array: [Int] { get set }
}

protocol RefP: AnyObject, P {
  var array: [Int] { get set }
}

class A: RefP {
  var array: [Int] = []
}

struct S {
  let value: RefP

  func append() {
    value.array.append(42)
  }
}

Okay the original question still remains through. ;) I really want to know if the "existential" itself is a ref or value type. :slight_smile:

Your question sounds strange to me. Any given existential (except those that require AnyObject) can wrap both value and reference types, and can only be observed through the content of their witness table. I consider the actual implementation of an existential as an implementation detail I would never dare relying upon.

But of course curiosity is always good, and I'll let knowledgable people provide a better answer.

Sure but any P is still a type like P.Type and P.Protocol or () -> Void. The question is if any P is a ref type or not. Closures and meta types are known to be ref types.

Uh, this answer is a bit nuanced, but here we go.

Existentials behave like a struct holding a single object that forwards all operations to that object. That means that the existential itself is a value, but operations on it have the semantics of the object it holds. If you have a class in the existential, while the existential box is a value, all operations on that value behave like a class. This is the same as having a struct holding a single instance variable that is a class, where all its operations forward to the class: while the outer type is technically a value, it behaves like a reference.

One extra wrinkle though: existentials can, in some circumstances, allocate storage on the heap. This is a property usually associated with classes (and thus references), but it's not actually related to the specific semantic of a type.

You can see this by running the following program:

protocol P {
    var x: Int { get set }
}

struct MyStruct: P {
    var x: Int = 0
}

class MyClass: P {
    var x: Int = 0
}

func returnAStruct() -> P {
    return MyStruct()
}

func returnAClass() -> P {
    return MyClass()
}

var s1 = returnAStruct()
let s2 = s1

s1.x = 5
print(s1.x)
print(s2.x)

var c1 = returnAClass()
let c2 = c1

c1.x = 5
print(c1.x)
print(c2.x)

This prints:

5
0
5
5

That is, an existential backed by a class behaves like a reference, and one backed by a struct behaves like a value.

5 Likes

Thank you for clarifying that to me. The behavior of the existential which holds a class is expected because the existential is basically a value type without value semantics. It will have value semantics if it holds a value that has value semantics, but that‘s just trivial implication of such simple value wrapper.

Since we know that the existential itself is a value the compiler errors in the original examples make totally sense, regardless the self mutation.

1 Like

As you said, the reality is a bit more nuanced:

c2.x = 1

If c2 were an actual class, not an existential backed by a class, this would compile just fine. Making returnAClass return P & AnyObject doesn't change this. On the other hand, using P & AnyObject does allow you to make c1 or c2 weak.

So yeah, existential types could do with a bit more documentation at least. Their behavior is quite confusing.

Protocols can be class-bound, too, in which case c2.x = 1 would be okay again. I think @lukasa's explanation is reasonably correct: were you to manually implement a generalized forwarding struct for a protocol with settable properties, you'd have to mark the setter as mutating, just in case the underlying type has a mutating setter.

2 Likes

@jrose Can you explain why this works (using P: AnyObject):

protocol P: AnyObject {
    var x: Int { get set }
}

class MyClass: P {
    var x: Int = 0
}

func returnAClass() -> P {
    return MyClass()
}

let c1 = returnAClass()
c1.x = 1

but not this (using P & AnyObject):

protocol P {
    var x: Int { get set }
}

class MyClass: P {
    var x: Int = 0
}

func returnAClass() -> P & AnyObject {
    return MyClass()
}

let c1 = returnAClass()
c1.x = 1

Shouldn't the compiler know that in both instances, c1 is a class instance?

1 Like

c1 being a class instance doesn't mean that x's setter isn't mutating (see the discussion on SR-142). It's a weird corner-case part of the language that I wish we'd done something about, but at this point it's behaving correctly.

1 Like

There is hidden achievement to unlock in this setup.

First, we know that the "existential" type itself is a value type which has no value semantics unless it wraps another type with value semantics.

struct Existential<Value> {
  // invisible to the user
  var _value: Value
  // forwards all members of Value
}

Now let's see our protocols:

protocol P0 {
  // this is a `mutating set`
  var x: Int { get set }
}

// protocol is class bound but it still has a `mutating set`
protocol P1: AnyObject, P0 {}

// protocol is class bound but now we re-defined the property
// which implies that the `set` becomes `nonmutating`, it's just
// implicit in this context
protocol P2: AnyObject, P0 {
  var x: Int { get set }
}

// Non-class bound protocol with explicit `nonmutating set`
protocol P3: P0 {
  var x: Int { get nonmutating set }
}

// class bound and implicit `nonmutating set` like P2
protocol P4: AnyObject {
  var x: Int { get set }
}

Now think about how the following values would behave:

Existential<P0>.x = 1 // expected-error
Existential<P1>.x = 1 // expected-error
Existential<P2>.x = 1 // okay
Existential<P3>.x = 1 // okay 
Existential<P4>.x = 1 // okay - this is your first example

// your second example
Existential<P0 & AnyObject>.x = 1 // expected-error
1 Like
Terms of Service

Privacy Policy

Cookie Policy