Preserving non-mutability of methods of an existential or generic object

The title is confusing, let me clarify by example:

We have this protocol with a mutating method:

protocol P { mutating func f(_ x: Int) -> Int }

And a conforming class (which has to conform with a non-mutating method):

class C: P { func f(_ x: Int) -> Int { return x } }

An instance of this class can be used with a let constant:

let c = C()
c.f(1) // OK

If we make it an existential object conforming to P, the immutability of the method will be erased:

let c: AnyObject & P = C()
c.f(1) // Cannot use mutating member on immutable value: 'c' is a 'let' constant

A generic context has the same issue:

func f<T: AnyObject & P>(_ arg: T)-> Int { return arg.f(1) } // Cannot use mutating member on immutable value: ‘arg' is a 'let' constant

My question:

Is it too much work to preserve method non-mutability in in these cases?

The workaround I am using is this:

protocol Q: class, P { func f(_ x: Int) -> Int } // 'Refine' it to be non-mutating.
extension C: Q {}

// Now these work:
let c: Q = C()
c.f(1) // OK
func f<T: Q>(_ arg: T)-> Int { return arg.f(1) } // OK

This workaround creates a lot of duplication and is hard to maintain. It is not something that I do often, but when I do, it is pretty annoying.

Supplemental questions:

Have you guys ever ran into this?
Is there already a bug tracking this?

Hi Hooman,

Since the protocol P is not class-bounded, the requirement can be witnessed by a protocol extension method which re-assigns ‘self’:

protocol Initable {
  init()
}

extension P where Self : Initable {
  mutating func f(_ x: Int) -> Int {
    self = Self()
    return x
  }
}

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

Now imagine you could do this,

let x: P & AnyObject

x.f(12)

This would be invalid because ‘x’ is a let binding but the requirement ‘f’ is witnessed by the protocol extension method, which performs a mutating access of ‘self’.

Slava

···

On Dec 21, 2017, at 6:01 PM, Hooman Mehr via swift-evolution <swift-evolution@swift.org> wrote:

The title is confusing, let me clarify by example:

We have this protocol with a mutating method:

protocol P { mutating func f(_ x: Int) -> Int }

And a conforming class (which has to conform with a non-mutating method):

class C: P { func f(_ x: Int) -> Int { return x } }

An instance of this class can be used with a let constant:

let c = C()
c.f(1) // OK

If we make it an existential object conforming to P, the immutability of the method will be erased:

let c: AnyObject & P = C()
c.f(1) // Cannot use mutating member on immutable value: 'c' is a 'let' constant

A generic context has the same issue:

func f<T: AnyObject & P>(_ arg: T)-> Int { return arg.f(1) } // Cannot use mutating member on immutable value: ‘arg' is a 'let' constant

My question:

Is it too much work to preserve method non-mutability in in these cases?

The workaround I am using is this:

protocol Q: class, P { func f(_ x: Int) -> Int } // 'Refine' it to be non-mutating.
extension C: Q {}

// Now these work:
let c: Q = C()
c.f(1) // OK
func f<T: Q>(_ arg: T)-> Int { return arg.f(1) } // OK

This workaround creates a lot of duplication and is hard to maintain. It is not something that I do often, but when I do, it is pretty annoying.

Supplemental questions:

Have you guys ever ran into this?
Is there already a bug tracking this?

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

1 Like

Thank you Slava, it is a very insightful answer.

It also reveals a potential source for hard to track bugs. To make it easier to see, lets add state to class C:

class D: C { var state = 0 }

var d = D() // Bad but common habit of declaring objects as var
for i in 1...5 { d.f(i); d.state += 1 }
print(d.state) // Prints 1

The result is surprising because a typical programmer does not expect an object to have mutating methods (that replace the object itself with a different one).

I think this is a bug in Swift compiler. It should not let a class “inherit” a mutating method in this manner. Compiler should require a non-mutating declaration of `f()` to satisfy `P` conformance for a class type.

If we fix the above, it should be possible to do what I asked in my post: When compiler knows an existential is an object, it should know all methods are non-mutating and accept `let` declaration despite calls to nominally mutating methods. Also, if a protocol refines another protocol to be class-bound, compiler should automatically refine all of its inherited mutating methods to be non-mutating and allow `let` declaration of an existential of that protocol even if there are calls to those originally mutating methods.

Hooman

···

On Dec 21, 2017, at 10:59 PM, Slava Pestov <spestov@apple.com> wrote:

Hi Hooman,

Since the protocol P is not class-bounded, the requirement can be witnessed by a protocol extension method which re-assigns ‘self’:

protocol Initable {
  init()
}

extension P where Self : Initable {
  mutating func f(_ x: Int) -> Int {
    self = Self()
    return x
  }
}

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

Now imagine you could do this,

let x: P & AnyObject

x.f(12)

This would be invalid because ‘x’ is a let binding but the requirement ‘f’ is witnessed by the protocol extension method, which performs a mutating access of ‘self’.

Slava

On Dec 21, 2017, at 6:01 PM, Hooman Mehr via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

The title is confusing, let me clarify by example:

We have this protocol with a mutating method:

protocol P { mutating func f(_ x: Int) -> Int }

And a conforming class (which has to conform with a non-mutating method):

class C: P { func f(_ x: Int) -> Int { return x } }

An instance of this class can be used with a let constant:

let c = C()
c.f(1) // OK

If we make it an existential object conforming to P, the immutability of the method will be erased:

let c: AnyObject & P = C()
c.f(1) // Cannot use mutating member on immutable value: 'c' is a 'let' constant

A generic context has the same issue:

func f<T: AnyObject & P>(_ arg: T)-> Int { return arg.f(1) } // Cannot use mutating member on immutable value: ‘arg' is a 'let' constant

My question:

Is it too much work to preserve method non-mutability in in these cases?

The workaround I am using is this:

protocol Q: class, P { func f(_ x: Int) -> Int } // 'Refine' it to be non-mutating.
extension C: Q {}

// Now these work:
let c: Q = C()
c.f(1) // OK
func f<T: Q>(_ arg: T)-> Int { return arg.f(1) } // OK

This workaround creates a lot of duplication and is hard to maintain. It is not something that I do often, but when I do, it is pretty annoying.

Supplemental questions:

Have you guys ever ran into this?
Is there already a bug tracking this?

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

Thank you Slava, it is a very insightful answer.

It also reveals a potential source for hard to track bugs. To make it easier to see, lets add state to class C:

class D: C { var state = 0 }

var d = D() // Bad but common habit of declaring objects as var
for i in 1...5 { d.f(i); d.state += 1 }
print(d.state) // Prints 1

The result is surprising because a typical programmer does not expect an object to have mutating methods (that replace the object itself with a different one).

I think this is a bug in Swift compiler. It should not let a class “inherit” a mutating method in this manner. Compiler should require a non-mutating declaration of `f()` to satisfy `P` conformance for a class type.

Perhaps, but it’s also possible to define a mutating method g() in a protocol extension of P, where g() is not a protocol requirement. Calling g() on a class instance can “replace” self in this manner also.

If we fix the above, it should be possible to do what I asked in my post: When compiler knows an existential is an object, it should know all methods are non-mutating and accept `let` declaration despite calls to nominally mutating methods. Also, if a protocol refines another protocol to be class-bound, compiler should automatically refine all of its inherited mutating methods to be non-mutating and allow `let` declaration of an existential of that protocol even if there are calls to those originally mutating methods.

This will prevent you from defining default implementations of protocol requirements in protocol extensions, which would be a source-breaking change at this point. I don’t think we can make such a change now.

Slava

···

On Jan 3, 2018, at 5:33 PM, Hooman Mehr <hooman@mac.com> wrote:

Hooman

On Dec 21, 2017, at 10:59 PM, Slava Pestov <spestov@apple.com <mailto:spestov@apple.com>> wrote:

Hi Hooman,

Since the protocol P is not class-bounded, the requirement can be witnessed by a protocol extension method which re-assigns ‘self’:

protocol Initable {
  init()
}

extension P where Self : Initable {
  mutating func f(_ x: Int) -> Int {
    self = Self()
    return x
  }
}

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

Now imagine you could do this,

let x: P & AnyObject

x.f(12)

This would be invalid because ‘x’ is a let binding but the requirement ‘f’ is witnessed by the protocol extension method, which performs a mutating access of ‘self’.

Slava

On Dec 21, 2017, at 6:01 PM, Hooman Mehr via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

The title is confusing, let me clarify by example:

We have this protocol with a mutating method:

protocol P { mutating func f(_ x: Int) -> Int }

And a conforming class (which has to conform with a non-mutating method):

class C: P { func f(_ x: Int) -> Int { return x } }

An instance of this class can be used with a let constant:

let c = C()
c.f(1) // OK

If we make it an existential object conforming to P, the immutability of the method will be erased:

let c: AnyObject & P = C()
c.f(1) // Cannot use mutating member on immutable value: 'c' is a 'let' constant

A generic context has the same issue:

func f<T: AnyObject & P>(_ arg: T)-> Int { return arg.f(1) } // Cannot use mutating member on immutable value: ‘arg' is a 'let' constant

My question:

Is it too much work to preserve method non-mutability in in these cases?

The workaround I am using is this:

protocol Q: class, P { func f(_ x: Int) -> Int } // 'Refine' it to be non-mutating.
extension C: Q {}

// Now these work:
let c: Q = C()
c.f(1) // OK
func f<T: Q>(_ arg: T)-> Int { return arg.f(1) } // OK

This workaround creates a lot of duplication and is hard to maintain. It is not something that I do often, but when I do, it is pretty annoying.

Supplemental questions:

Have you guys ever ran into this?
Is there already a bug tracking this?

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

Thank you Slava, it is a very insightful answer.
It also reveals a potential source for hard to track bugs. To make it easier to see, lets add state to class C:

class D: C { var state = 0 }
var d = D() // Bad but common habit of declaring objects as var
for i in 1...5 { d.f(i); d.state += 1 }
print(d.state) // Prints 1

The result is surprising because a typical programmer does not expect an object to have mutating methods (that replace the object itself with a different one).
I think this is a bug in Swift compiler. It should not let a class “inherit” a mutating method in this manner. Compiler should require a non-mutating declaration of `f()` to satisfy `P` conformance for a class type.

Perhaps, but it’s also possible to define a mutating method g() in a protocol extension of P, where g() is not a protocol requirement. Calling g() on a class instance can “replace” self in this manner also.

Is this desirable? It seems that Swift compiler generally tries to prevent you from defining mutating methods on class types and this situation feels like a flaw/bug that needs fixing. I think this can even have security implications (letting a malicious coder obfuscate a code put in place to create a backdoor).

If we fix the above, it should be possible to do what I asked in my post: When compiler knows an existential is an object, it should know all methods are non-mutating and accept `let` declaration despite calls to nominally mutating methods. Also, if a protocol refines another protocol to be class-bound, compiler should automatically refine all of its inherited mutating methods to be non-mutating and allow `let` declaration of an existential of that protocol even if there are calls to those originally mutating methods.

This will prevent you from defining default implementations of protocol requirements in protocol extensions, which would be a source-breaking change at this point. I don’t think we can make such a change now.

I don’t think a mutating default implementation makes any sense for a class type. I think we should handle default implementation for class type with a constrained/refined extension that provides a non-mutating default implementation for the cases when a class type conforms to a non-class bound protocol. This seems to be impossible at the moment, but I see it as another shortcoming in the compiler, not a source breaking change for the sake of providing an obscure new feature.

···

On Jan 3, 2018, at 6:45 PM, Slava Pestov <spestov@apple.com> wrote:

On Jan 3, 2018, at 5:33 PM, Hooman Mehr <hooman@mac.com <mailto:hooman@mac.com>> wrote:

Slava

Hooman

On Dec 21, 2017, at 10:59 PM, Slava Pestov <spestov@apple.com <mailto:spestov@apple.com>> wrote:

Hi Hooman,

Since the protocol P is not class-bounded, the requirement can be witnessed by a protocol extension method which re-assigns ‘self’:

protocol Initable {
  init()
}

extension P where Self : Initable {
  mutating func f(_ x: Int) -> Int {
    self = Self()
    return x
  }
}

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

Now imagine you could do this,

let x: P & AnyObject

x.f(12)

This would be invalid because ‘x’ is a let binding but the requirement ‘f’ is witnessed by the protocol extension method, which performs a mutating access of ‘self’.

Slava

On Dec 21, 2017, at 6:01 PM, Hooman Mehr via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

The title is confusing, let me clarify by example:

We have this protocol with a mutating method:

protocol P { mutating func f(_ x: Int) -> Int }

And a conforming class (which has to conform with a non-mutating method):

class C: P { func f(_ x: Int) -> Int { return x } }

An instance of this class can be used with a let constant:

let c = C()
c.f(1) // OK

If we make it an existential object conforming to P, the immutability of the method will be erased:

let c: AnyObject & P = C()
c.f(1) // Cannot use mutating member on immutable value: 'c' is a 'let' constant

A generic context has the same issue:

func f<T: AnyObject & P>(_ arg: T)-> Int { return arg.f(1) } // Cannot use mutating member on immutable value: ‘arg' is a 'let' constant

My question:

Is it too much work to preserve method non-mutability in in these cases?

The workaround I am using is this:

protocol Q: class, P { func f(_ x: Int) -> Int } // 'Refine' it to be non-mutating.
extension C: Q {}

// Now these work:
let c: Q = C()
c.f(1) // OK
func f<T: Q>(_ arg: T)-> Int { return arg.f(1) } // OK

This workaround creates a lot of duplication and is hard to maintain. It is not something that I do often, but when I do, it is pretty annoying.

Supplemental questions:

Have you guys ever ran into this?
Is there already a bug tracking this?

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution