Class mutation model and value-constrained protocols


(Jon Hull) #1

Comments inline

A “mutating” keyword on a protocol method is only useful if
you can reason about the mutation in generic code.

  protocol P {
    mutating func changeMe()
    func noChange() -> Int
  }

In other words, given x of some type conforming to P, it should be
meaningful that this is an error:

  func f<T: P>(x: T) {
    immutable.changeMe() // can't mutate a "let" binding
  }

Agreed.

which means that you shouldn't be able to get around that meaning by
writing:

  func g<T: P>(x: T) {
    var mutable = x
    mutable.changeMe() // OK
  }

Now I am confused. Why should this be illegal? Doesn’t this work with structs right now? (you are working on a mutable copy)

Also, you should be able to reason that both of the following print the same
thing twice, for types whose methods have no external side-effects:

  func gg<T: P>(x: T) {
    print(x)
    x.noChange()
    print(x)
  }

  func ggg<T: P>(x: T) {
    print(x)
    var y = x
    y.changeMe()
    print(x)
  }

I see the issue here. Though, it seems to me it is just a more easily discoverable version of an entire class of problem with reference types in general. X could be changed on a different thread too, for example. or in a nested function call.

You would also still have the same problem with 'let y = x'.

When T is a class type, it can easily violate *all* of these
expectations. In other words, classes naturally bypass the mutation
model.

If we are going to maintain source stability after Swift 3, it seems
that we either need to address this now, or decide that it won't be
addressed, because of the “viral const” problem.

One idea that Jordan and I have floated is that protocols with mutating
methods should be constrained to applying to non-class types. That
would be a step in the right direction, but, that still leaves cases
like gg able to easily violate expectations when the protocol in
question has no mutating methods.

I really *dislike* the approach of disallowing class types for protocols with mutating methods, unless an additional reference type is added. I have several protocols which have conforming classes and structs and that that lets you choose reference vs value semantics.

I would much rather have us mark class methods as mutating when they change the class’s value, and just having the concept be separate from let/var in that case.

Another possibility would be to formalize the idea of value semantics in
protocol declarations, so that non-class protocols were only allowed to
apply to values.

I would like to have a way to require value semantics in a protocol (similar to how we can require ‘class' now). I still really want/need the ability to have a protocol which can be adhered to by both value and class types though...

It's also possible that I've overestimated the seriousness of the issue
and we actually can afford to postpone thinking about it until after
Swift 4.

Thoughts?

I would vote to postpone to swift 4, and have it be part of a larger discussion involving the marking of side-effects and thread safe mutability.

If you need a stop-gap for Swift 3, I would be in favor of adding the ability to mark a particular protocol as needing to be a value type (not every protocol… just those that are marked as such). That should give you the guarantees you need for particular projects.

Thanks,
Jon


(Dave Abrahams) #2

Comments inline

A “mutating” keyword on a protocol method is only useful if
you can reason about the mutation in generic code.

  protocol P {
    mutating func changeMe()
    func noChange() -> Int
  }

In other words, given x of some type conforming to P, it should be
meaningful that this is an error:

  func f<T: P>(x: T) {
    immutable.changeMe() // can't mutate a "let" binding
  }

Agreed.

which means that you shouldn't be able to get around that meaning by
writing:

  func g<T: P>(x: T) {
    var mutable = x
    mutable.changeMe() // OK
  }

Now I am confused. Why should this be illegal? Doesn’t this work with structs right now? (you are working on a mutable copy)

What I meant by “you shouldn't be able get around that” was that the
code shouldn't change the value of x.

Also, you should be able to reason that both of the following print the same
thing twice, for types whose methods have no external side-effects:

  func gg<T: P>(x: T) {
    print(x)
    x.noChange()
    print(x)
  }

  func ggg<T: P>(x: T) {
    print(x)
    var y = x
    y.changeMe()
    print(x)
  }

I see the issue here. Though, it seems to me it is just a more easily
discoverable version of an entire class of problem with reference
types in general. X could be changed on a different thread too, for
example. or in a nested function call.

You would also still have the same problem with 'let y = x'.

When T is a class type, it can easily violate *all* of these
expectations. In other words, classes naturally bypass the mutation
model.

If we are going to maintain source stability after Swift 3, it seems
that we either need to address this now, or decide that it won't be
addressed, because of the “viral const” problem.

One idea that Jordan and I have floated is that protocols with mutating
methods should be constrained to applying to non-class types. That
would be a step in the right direction, but, that still leaves cases
like gg able to easily violate expectations when the protocol in
question has no mutating methods.

I really *dislike* the approach of disallowing class types for
protocols with mutating methods, unless an additional reference type
is added. I have several protocols which have conforming classes and
structs and that that lets you choose reference vs value semantics.

How do you write generic code that works on both reference and value
types conforming to such a protocol?

I would much rather have us mark class methods as mutating when they
change the class’s value, and just having the concept be separate from
let/var in that case.

Another possibility would be to formalize the idea of value semantics in
protocol declarations, so that non-class protocols were only allowed to
apply to values.

I would like to have a way to require value semantics in a protocol
(similar to how we can require ‘class' now). I still really want/need
the ability to have a protocol which can be adhered to by both value
and class types though...

I really want to see examples of generic code that isn't terribly tricky
to write correctly so it will work on both kinds of type.

It's also possible that I've overestimated the seriousness of the issue
and we actually can afford to postpone thinking about it until after
Swift 4.

Thoughts?

I would vote to postpone to swift 4, and have it be part of a larger
discussion involving the marking of side-effects and thread safe
mutability.

If you need a stop-gap for Swift 3, I would be in favor of adding the
ability to mark a particular protocol as needing to be a value type
(not every protocol… just those that are marked as such). That should
give you the guarantees you need for particular projects.

That's a reasonable stopping point.

···

on Tue Jul 05 2016, Jonathan Hull <jhull-AT-gbis.com> wrote:

--
Dave


(Sean Heber) #3

One idea that Jordan and I have floated is that protocols with mutating
methods should be constrained to applying to non-class types. That
would be a step in the right direction, but, that still leaves cases
like gg able to easily violate expectations when the protocol in
question has no mutating methods.

I really *dislike* the approach of disallowing class types for protocols with mutating methods, unless an additional reference type is added. I have several protocols which have conforming classes and structs and that that lets you choose reference vs value semantics.

I would much rather have us mark class methods as mutating when they change the class’s value, and just having the concept be separate from let/var in that case.

Agreed - I think it’d make more sense this way, too. In some ways, I’m not sure why we don’t just require all mutating methods - even in classes - be declared as such, but perhaps that’s a can of worms and/or boilerplate?

Another possibility would be to formalize the idea of value semantics in
protocol declarations, so that non-class protocols were only allowed to
apply to values.

I would like to have a way to require value semantics in a protocol (similar to how we can require ‘class' now). I still really want/need the ability to have a protocol which can be adhered to by both value and class types though...

+1

l8r
Sean


(Jon Hull) #4

Comments inline.

Comments inline

A “mutating” keyword on a protocol method is only useful if
you can reason about the mutation in generic code.

protocol P {
   mutating func changeMe()
   func noChange() -> Int
}

In other words, given x of some type conforming to P, it should be
meaningful that this is an error:

func f<T: P>(x: T) {
   immutable.changeMe() // can't mutate a "let" binding
}

Agreed.

which means that you shouldn't be able to get around that meaning by
writing:

func g<T: P>(x: T) {
   var mutable = x
   mutable.changeMe() // OK
}

Now I am confused. Why should this be illegal? Doesn’t this work with structs right now? (you are working on a mutable copy)

What I meant by “you shouldn't be able get around that” was that the
code shouldn't change the value of x.

Ah. Gotcha.

In that case, my same comments apply re: general issues of confusion around reference types. If x is a reference type, then you should even be able to call changeMe() without creating a mutable copy, no?

It sounds like what you are really wanting here is some notion of purity. All of the same arguments from above could be made about side effects in general (of which mutation is one example).

Also, you should be able to reason that both of the following print the same
thing twice, for types whose methods have no external side-effects:

func gg<T: P>(x: T) {
   print(x)
   x.noChange()
   print(x)
}

func ggg<T: P>(x: T) {
   print(x)
   var y = x
   y.changeMe()
   print(x)
}

I see the issue here. Though, it seems to me it is just a more easily
discoverable version of an entire class of problem with reference
types in general. X could be changed on a different thread too, for
example. or in a nested function call.

You would also still have the same problem with 'let y = x'.

When T is a class type, it can easily violate *all* of these
expectations. In other words, classes naturally bypass the mutation
model.

If we are going to maintain source stability after Swift 3, it seems
that we either need to address this now, or decide that it won't be
addressed, because of the “viral const” problem.

One idea that Jordan and I have floated is that protocols with mutating
methods should be constrained to applying to non-class types. That
would be a step in the right direction, but, that still leaves cases
like gg able to easily violate expectations when the protocol in
question has no mutating methods.

I really *dislike* the approach of disallowing class types for
protocols with mutating methods, unless an additional reference type
is added. I have several protocols which have conforming classes and
structs and that that lets you choose reference vs value semantics.

How do you write generic code that works on both reference and value
types conforming to such a protocol?

I don’t make those assumptions around mutability. i.e. I know it *could* be a reference type and account for those cases.

I do like the idea of having a way to specify that a protocol can only apply to a value type for the cases where those guarantees around mutability are needed.

···

On Jul 5, 2016, at 2:11 PM, Dave Abrahams <dabrahams@apple.com> wrote:
on Tue Jul 05 2016, Jonathan Hull <jhull-AT-gbis.com <http://jhull-at-gbis.com/>> wrote:

I would much rather have us mark class methods as mutating when they
change the class’s value, and just having the concept be separate from
let/var in that case.

Another possibility would be to formalize the idea of value semantics in
protocol declarations, so that non-class protocols were only allowed to
apply to values.

I would like to have a way to require value semantics in a protocol
(similar to how we can require ‘class' now). I still really want/need
the ability to have a protocol which can be adhered to by both value
and class types though...

I really want to see examples of generic code that isn't terribly tricky
to write correctly so it will work on both kinds of type.

It's also possible that I've overestimated the seriousness of the issue
and we actually can afford to postpone thinking about it until after
Swift 4.

Thoughts?

I would vote to postpone to swift 4, and have it be part of a larger
discussion involving the marking of side-effects and thread safe
mutability.

If you need a stop-gap for Swift 3, I would be in favor of adding the
ability to mark a particular protocol as needing to be a value type
(not every protocol… just those that are marked as such). That should
give you the guarantees you need for particular projects.

That's a reasonable stopping point.

--
Dave


(Rod Brown) #5

I definitely see Dave’s point here.

It seems to me we can only get generic to the point at which the semantics change. So methods that don’t have any mutating consequences can be applied to any value such as Structs and Classes, but when the semantic relates to mutation, we can only apply that to “All structs” or “all classes” as the behaviour changes dramatically how we deal with it. I can’t see any way around that, but I’d love to see any ideas how we could avoid it.

I think this is definitely somewhere we need a way to declare a protocol can only be implemented on a value type, much like we can say that protocol is only available on classes today.

- Rod

···

On 6 Jul 2016, at 7:11 AM, Dave Abrahams via swift-evolution <swift-evolution@swift.org> wrote:

on Tue Jul 05 2016, Jonathan Hull <jhull-AT-gbis.com <http://jhull-at-gbis.com/>> wrote:

Comments inline

A “mutating” keyword on a protocol method is only useful if
you can reason about the mutation in generic code.

protocol P {
   mutating func changeMe()
   func noChange() -> Int
}

In other words, given x of some type conforming to P, it should be
meaningful that this is an error:

func f<T: P>(x: T) {
   immutable.changeMe() // can't mutate a "let" binding
}

Agreed.

which means that you shouldn't be able to get around that meaning by
writing:

func g<T: P>(x: T) {
   var mutable = x
   mutable.changeMe() // OK
}

Now I am confused. Why should this be illegal? Doesn’t this work with structs right now? (you are working on a mutable copy)

What I meant by “you shouldn't be able get around that” was that the
code shouldn't change the value of x.

Also, you should be able to reason that both of the following print the same
thing twice, for types whose methods have no external side-effects:

func gg<T: P>(x: T) {
   print(x)
   x.noChange()
   print(x)
}

func ggg<T: P>(x: T) {
   print(x)
   var y = x
   y.changeMe()
   print(x)
}

I see the issue here. Though, it seems to me it is just a more easily
discoverable version of an entire class of problem with reference
types in general. X could be changed on a different thread too, for
example. or in a nested function call.

You would also still have the same problem with 'let y = x'.

When T is a class type, it can easily violate *all* of these
expectations. In other words, classes naturally bypass the mutation
model.

If we are going to maintain source stability after Swift 3, it seems
that we either need to address this now, or decide that it won't be
addressed, because of the “viral const” problem.

One idea that Jordan and I have floated is that protocols with mutating
methods should be constrained to applying to non-class types. That
would be a step in the right direction, but, that still leaves cases
like gg able to easily violate expectations when the protocol in
question has no mutating methods.

I really *dislike* the approach of disallowing class types for
protocols with mutating methods, unless an additional reference type
is added. I have several protocols which have conforming classes and
structs and that that lets you choose reference vs value semantics.

How do you write generic code that works on both reference and value
types conforming to such a protocol?

I would much rather have us mark class methods as mutating when they
change the class’s value, and just having the concept be separate from
let/var in that case.

Another possibility would be to formalize the idea of value semantics in
protocol declarations, so that non-class protocols were only allowed to
apply to values.

I would like to have a way to require value semantics in a protocol
(similar to how we can require ‘class' now). I still really want/need
the ability to have a protocol which can be adhered to by both value
and class types though...

I really want to see examples of generic code that isn't terribly tricky
to write correctly so it will work on both kinds of type.

It's also possible that I've overestimated the seriousness of the issue
and we actually can afford to postpone thinking about it until after
Swift 4.

Thoughts?

I would vote to postpone to swift 4, and have it be part of a larger
discussion involving the marking of side-effects and thread safe
mutability.

If you need a stop-gap for Swift 3, I would be in favor of adding the
ability to mark a particular protocol as needing to be a value type
(not every protocol… just those that are marked as such). That should
give you the guarantees you need for particular projects.

That's a reasonable stopping point.

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