[Pitch] Adding a `mutate` clause to computed properties and subscripts


(Tim Vermeulen) #1

There have been many instances where unexpected bad performance was caused by the interplay between getters, setters, mutations and the copy-on-write mechanism. For example:

struct Foo {
    private var _array: [Int] = [1, 2, 3, 4, 5]
    
    var array: [Int] {
        get { return _array }
        set { _array = newValue }
    }
}

var foo = Foo()
foo.array.append(6) // an O(n) operation

I propose a `mutate` clause which provides a `mutate` function with a single inout parameter (similar to how `set` provides `newValue`), which can be used instead of a setter:

var array: [Int] {
    get { return _array }
    mutate { mutate(&_array) }
}

The compiler could then translate each mutation of `foo.array` to a closure with an inout parameter, which is then passed into the `mutate` clause (which in turn is executed with `_array` as its argument, as per the snippet above). For example, for `foo.array.append(6)`, the compiler would internally generate the closure `{ (arr: inout [Int]) in arr.append(6) }` and pass it into the `mutate` clause, `_array` is then passed as its parameter and the array is updated in constant time.

I apologise if that was too hard to follow.

No setter would be needed if a mutation clause is provided, but I see no good reason to do away with setters altogether, so this proposal would be purely additive.


(Erica Sadun) #2

If this is computationally better, why is it not the default behavior rather than an API change?

-- E

···

On Oct 10, 2016, at 9:53 PM, Tim Vermeulen via swift-evolution <swift-evolution@swift.org> wrote:

There have been many instances where unexpected bad performance was caused by the interplay between getters, setters, mutations and the copy-on-write mechanism. For example:

struct Foo {
    private var _array: [Int] = [1, 2, 3, 4, 5]
    
    var array: [Int] {
        get { return _array }
        set { _array = newValue }
    }
}

var foo = Foo()
foo.array.append(6) // an O(n) operation

I propose a `mutate` clause which provides a `mutate` function with a single inout parameter (similar to how `set` provides `newValue`), which can be used instead of a setter:

var array: [Int] {
    get { return _array }
    mutate { mutate(&_array) }
}

The compiler could then translate each mutation of `foo.array` to a closure with an inout parameter, which is then passed into the `mutate` clause (which in turn is executed with `_array` as its argument, as per the snippet above). For example, for `foo.array.append(6)`, the compiler would internally generate the closure `{ (arr: inout [Int]) in arr.append(6) }` and pass it into the `mutate` clause, `_array` is then passed as its parameter and the array is updated in constant time.

I apologise if that was too hard to follow.

No setter would be needed if a mutation clause is provided, but I see no good reason to do away with setters altogether, so this proposal would be purely additive.


(Tim Vermeulen) #3

Just having getters and setters doesn’t allow this (unless the optimiser is really smart about it). All the current API allows is grabbing whatever the `get` clause returns, mutating it, and then passing it into the `set` clause, whatever that does. The `set` clause might not do anything, or what it does could be seemingly unrelated to the `get` clause, so it’s not a trivial task to optimise this.

···

On 11 Oct 2016, at 06:35, Erica Sadun <erica@ericasadun.com> wrote:

On Oct 10, 2016, at 9:53 PM, Tim Vermeulen via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

There have been many instances where unexpected bad performance was caused by the interplay between getters, setters, mutations and the copy-on-write mechanism. For example:

struct Foo {
    private var _array: [Int] = [1, 2, 3, 4, 5]
    
    var array: [Int] {
        get { return _array }
        set { _array = newValue }
    }
}

var foo = Foo()
foo.array.append(6) // an O(n) operation

I propose a `mutate` clause which provides a `mutate` function with a single inout parameter (similar to how `set` provides `newValue`), which can be used instead of a setter:

var array: [Int] {
    get { return _array }
    mutate { mutate(&_array) }
}

The compiler could then translate each mutation of `foo.array` to a closure with an inout parameter, which is then passed into the `mutate` clause (which in turn is executed with `_array` as its argument, as per the snippet above). For example, for `foo.array.append(6)`, the compiler would internally generate the closure `{ (arr: inout [Int]) in arr.append(6) }` and pass it into the `mutate` clause, `_array` is then passed as its parameter and the array is updated in constant time.

I apologise if that was too hard to follow.

No setter would be needed if a mutation clause is provided, but I see no good reason to do away with setters altogether, so this proposal would be purely additive.

If this is computationally better, why is it not the default behavior rather than an API change?

-- E


(Jay) #4

This is interesting. I'm trying to evaluate your statement that "No setter
would be needed if a mutation clause is provided" but I can't think exactly
what the compiler would do in the case where there is a 'get' and 'mutate',
but no 'set'...
a) when you call a non-mutating function;
b) when you assign a new value.
c) when get and set aren't implemented with a matching hidden var (i.e.
using a bit in an int var or storing in a dictionary).

···

On Tue, 11 Oct 2016 at 11:26 Tim Vermeulen via swift-evolution < swift-evolution@swift.org> wrote:

Just having getters and setters doesn’t allow this (unless the optimiser
is really smart about it). All the current API allows is grabbing whatever
the `get` clause returns, mutating it, and then passing it into the `set`
clause, whatever that does. The `set` clause might not do anything, or what
it does could be seemingly unrelated to the `get` clause, so it’s not a
trivial task to optimise this.

On 11 Oct 2016, at 06:35, Erica Sadun <erica@ericasadun.com> wrote:

On Oct 10, 2016, at 9:53 PM, Tim Vermeulen via swift-evolution < > swift-evolution@swift.org> wrote:

There have been many instances where unexpected bad performance was caused
by the interplay between getters, setters, mutations and the copy-on-write
mechanism. For example:

struct Foo {
    private var _array: [Int] = [1, 2, 3, 4, 5]

    var array: [Int] {
        get { return _array }
        set { _array = newValue }
    }
}

var foo = Foo()
foo.array.append(6) // an O(n) operation

I propose a `mutate` clause which provides a `mutate` function with a
single inout parameter (similar to how `set` provides `newValue`), which
can be used instead of a setter:

var array: [Int] {
    get { return _array }
    mutate { mutate(&_array) }
}

The compiler could then translate each mutation of `foo.array` to a
closure with an inout parameter, which is then passed into the `mutate`
clause (which in turn is executed with `_array` as its argument, as per the
snippet above). For example, for `foo.array.append(6)`, the compiler would
internally generate the closure `{ (arr: inout [Int]) in arr.append(6) }`
and pass it into the `mutate` clause, `_array` is then passed as its
parameter and the array is updated in constant time.

I apologise if that was too hard to follow.

No setter would be needed if a mutation clause is provided, but I see no
good reason to do away with setters altogether, so this proposal would be
purely additive.

If this is computationally better, why is it not the default behavior
rather than an API change?

-- E

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


(Jay) #5

actually scratch point a) - that's bloody obvious :smiley:

···

On Tue, 11 Oct 2016 at 11:58 Jay Abbott <jay@abbott.me.uk> wrote:

This is interesting. I'm trying to evaluate your statement that "No setter
would be needed if a mutation clause is provided" but I can't think exactly
what the compiler would do in the case where there is a 'get' and 'mutate',
but no 'set'...
a) when you call a non-mutating function;
b) when you assign a new value.
c) when get and set aren't implemented with a matching hidden var (i.e.
using a bit in an int var or storing in a dictionary).

On Tue, 11 Oct 2016 at 11:26 Tim Vermeulen via swift-evolution < > swift-evolution@swift.org> wrote:

Just having getters and setters doesn’t allow this (unless the optimiser
is really smart about it). All the current API allows is grabbing whatever
the `get` clause returns, mutating it, and then passing it into the `set`
clause, whatever that does. The `set` clause might not do anything, or what
it does could be seemingly unrelated to the `get` clause, so it’s not a
trivial task to optimise this.

On 11 Oct 2016, at 06:35, Erica Sadun <erica@ericasadun.com> wrote:

On Oct 10, 2016, at 9:53 PM, Tim Vermeulen via swift-evolution < > swift-evolution@swift.org> wrote:

There have been many instances where unexpected bad performance was caused
by the interplay between getters, setters, mutations and the copy-on-write
mechanism. For example:

struct Foo {
    private var _array: [Int] = [1, 2, 3, 4, 5]

    var array: [Int] {
        get { return _array }
        set { _array = newValue }
    }
}

var foo = Foo()
foo.array.append(6) // an O(n) operation

I propose a `mutate` clause which provides a `mutate` function with a
single inout parameter (similar to how `set` provides `newValue`), which
can be used instead of a setter:

var array: [Int] {
    get { return _array }
    mutate { mutate(&_array) }
}

The compiler could then translate each mutation of `foo.array` to a
closure with an inout parameter, which is then passed into the `mutate`
clause (which in turn is executed with `_array` as its argument, as per the
snippet above). For example, for `foo.array.append(6)`, the compiler would
internally generate the closure `{ (arr: inout [Int]) in arr.append(6) }`
and pass it into the `mutate` clause, `_array` is then passed as its
parameter and the array is updated in constant time.

I apologise if that was too hard to follow.

No setter would be needed if a mutation clause is provided, but I see no
good reason to do away with setters altogether, so this proposal would be
purely additive.

If this is computationally better, why is it not the default behavior
rather than an API change?

-- E

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


(Jay) #6

In case c) for example if your set implementation was something like this:
set { _internalDict["propName"] = newValue }
set { if newValue { _int |= SOME_BIT } else { _int &= ~SOME_BIT } }
Could they be implemented as 'mutate'?

···

On Tue, 11 Oct 2016 at 12:05 Jay Abbott <jay@abbott.me.uk> wrote:

actually scratch point a) - that's bloody obvious :smiley:

On Tue, 11 Oct 2016 at 11:58 Jay Abbott <jay@abbott.me.uk> wrote:

This is interesting. I'm trying to evaluate your statement that "No setter
would be needed if a mutation clause is provided" but I can't think exactly
what the compiler would do in the case where there is a 'get' and 'mutate',
but no 'set'...
a) when you call a non-mutating function;
b) when you assign a new value.
c) when get and set aren't implemented with a matching hidden var (i.e.
using a bit in an int var or storing in a dictionary).

On Tue, 11 Oct 2016 at 11:26 Tim Vermeulen via swift-evolution < > swift-evolution@swift.org> wrote:

Just having getters and setters doesn’t allow this (unless the optimiser
is really smart about it). All the current API allows is grabbing whatever
the `get` clause returns, mutating it, and then passing it into the `set`
clause, whatever that does. The `set` clause might not do anything, or what
it does could be seemingly unrelated to the `get` clause, so it’s not a
trivial task to optimise this.

On 11 Oct 2016, at 06:35, Erica Sadun <erica@ericasadun.com> wrote:

On Oct 10, 2016, at 9:53 PM, Tim Vermeulen via swift-evolution < > swift-evolution@swift.org> wrote:

There have been many instances where unexpected bad performance was caused
by the interplay between getters, setters, mutations and the copy-on-write
mechanism. For example:

struct Foo {
    private var _array: [Int] = [1, 2, 3, 4, 5]

    var array: [Int] {
        get { return _array }
        set { _array = newValue }
    }
}

var foo = Foo()
foo.array.append(6) // an O(n) operation

I propose a `mutate` clause which provides a `mutate` function with a
single inout parameter (similar to how `set` provides `newValue`), which
can be used instead of a setter:

var array: [Int] {
    get { return _array }
    mutate { mutate(&_array) }
}

The compiler could then translate each mutation of `foo.array` to a
closure with an inout parameter, which is then passed into the `mutate`
clause (which in turn is executed with `_array` as its argument, as per the
snippet above). For example, for `foo.array.append(6)`, the compiler would
internally generate the closure `{ (arr: inout [Int]) in arr.append(6) }`
and pass it into the `mutate` clause, `_array` is then passed as its
parameter and the array is updated in constant time.

I apologise if that was too hard to follow.

No setter would be needed if a mutation clause is provided, but I see no
good reason to do away with setters altogether, so this proposal would be
purely additive.

If this is computationally better, why is it not the default behavior
rather than an API change?

-- E

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


(Tim Vermeulen) #7

This is interesting. I'm trying to evaluate your statement that "No setter would be needed if a mutation clause is provided" but I can't think exactly what the compiler would do in the case where there is a 'get' and 'mutate', but no 'set'...
a) when you call a non-mutating function;

This would just call the getter and call the function on the result, exactly how it works right now.

b) when you assign a new value.

A setter would be a special case of a “mutator”, where the starting value of the inout parameter is ignored. So the mutate clause would be invoked with `{ (x: inout T) in x = newValue }`. Does that make sense?

c) when get and set aren't implemented with a matching hidden var (i.e. using a bit in an int var or storing in a dictionary).

I’m not sure what you mean by this. The getter and setter are implemented, but the mutator isn’t? Could you clarify?

···

On 11 Oct 2016, at 12:59, Jay Abbott <jay@abbott.me.uk> wrote:

On Tue, 11 Oct 2016 at 11:26 Tim Vermeulen via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
Just having getters and setters doesn’t allow this (unless the optimiser is really smart about it). All the current API allows is grabbing whatever the `get` clause returns, mutating it, and then passing it into the `set` clause, whatever that does. The `set` clause might not do anything, or what it does could be seemingly unrelated to the `get` clause, so it’s not a trivial task to optimise this.

On 11 Oct 2016, at 06:35, Erica Sadun <erica@ericasadun.com <mailto:erica@ericasadun.com>> wrote:

On Oct 10, 2016, at 9:53 PM, Tim Vermeulen via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

There have been many instances where unexpected bad performance was caused by the interplay between getters, setters, mutations and the copy-on-write mechanism. For example:

struct Foo {
    private var _array: [Int] = [1, 2, 3, 4, 5]
    
    var array: [Int] {
        get { return _array }
        set { _array = newValue }
    }
}

var foo = Foo()
foo.array.append(6) // an O(n) operation

I propose a `mutate` clause which provides a `mutate` function with a single inout parameter (similar to how `set` provides `newValue`), which can be used instead of a setter:

var array: [Int] {
    get { return _array }
    mutate { mutate(&_array) }
}

The compiler could then translate each mutation of `foo.array` to a closure with an inout parameter, which is then passed into the `mutate` clause (which in turn is executed with `_array` as its argument, as per the snippet above). For example, for `foo.array.append(6)`, the compiler would internally generate the closure `{ (arr: inout [Int]) in arr.append(6) }` and pass it into the `mutate` clause, `_array` is then passed as its parameter and the array is updated in constant time.

I apologise if that was too hard to follow.

No setter would be needed if a mutation clause is provided, but I see no good reason to do away with setters altogether, so this proposal would be purely additive.

If this is computationally better, why is it not the default behavior rather than an API change?

-- E

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


(Tim Vermeulen) #8

Oh, I see. I’m not sure about the dictionary one, that probably depends on whether the `Dictionary` type could use this addition to make member mutations O(1). The other example probably wouldn’t benefit from replacing the setter by a mutator, as there’s nothing to gain here - the implementation is already optional. This is exactly why `set` shouldn’t be removed from the language :slight_smile:

`mutate` wouldn’t be able to replace each and every setter, but there's a lot of situations imaginable where it can.

···

On 11 Oct 2016, at 13:20, Jay Abbott <jay@abbott.me.uk> wrote:

In case c) for example if your set implementation was something like this:
set { _internalDict["propName"] = newValue }
set { if newValue { _int |= SOME_BIT } else { _int &= ~SOME_BIT } }
Could they be implemented as 'mutate'?

On Tue, 11 Oct 2016 at 12:05 Jay Abbott <jay@abbott.me.uk <mailto:jay@abbott.me.uk>> wrote:
actually scratch point a) - that's bloody obvious :smiley:

On Tue, 11 Oct 2016 at 11:58 Jay Abbott <jay@abbott.me.uk <mailto:jay@abbott.me.uk>> wrote:
This is interesting. I'm trying to evaluate your statement that "No setter would be needed if a mutation clause is provided" but I can't think exactly what the compiler would do in the case where there is a 'get' and 'mutate', but no 'set'...
a) when you call a non-mutating function;
b) when you assign a new value.
c) when get and set aren't implemented with a matching hidden var (i.e. using a bit in an int var or storing in a dictionary).

On Tue, 11 Oct 2016 at 11:26 Tim Vermeulen via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
Just having getters and setters doesn’t allow this (unless the optimiser is really smart about it). All the current API allows is grabbing whatever the `get` clause returns, mutating it, and then passing it into the `set` clause, whatever that does. The `set` clause might not do anything, or what it does could be seemingly unrelated to the `get` clause, so it’s not a trivial task to optimise this.

On 11 Oct 2016, at 06:35, Erica Sadun <erica@ericasadun.com <mailto:erica@ericasadun.com>> wrote:

On Oct 10, 2016, at 9:53 PM, Tim Vermeulen via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

There have been many instances where unexpected bad performance was caused by the interplay between getters, setters, mutations and the copy-on-write mechanism. For example:

struct Foo {
    private var _array: [Int] = [1, 2, 3, 4, 5]
    
    var array: [Int] {
        get { return _array }
        set { _array = newValue }
    }
}

var foo = Foo()
foo.array.append(6) // an O(n) operation

I propose a `mutate` clause which provides a `mutate` function with a single inout parameter (similar to how `set` provides `newValue`), which can be used instead of a setter:

var array: [Int] {
    get { return _array }
    mutate { mutate(&_array) }
}

The compiler could then translate each mutation of `foo.array` to a closure with an inout parameter, which is then passed into the `mutate` clause (which in turn is executed with `_array` as its argument, as per the snippet above). For example, for `foo.array.append(6)`, the compiler would internally generate the closure `{ (arr: inout [Int]) in arr.append(6) }` and pass it into the `mutate` clause, `_array` is then passed as its parameter and the array is updated in constant time.

I apologise if that was too hard to follow.

No setter would be needed if a mutation clause is provided, but I see no good reason to do away with setters altogether, so this proposal would be purely additive.

If this is computationally better, why is it not the default behavior rather than an API change?

-- E

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


(Joe Groff) #9

Specifically this section, which describes more or less exactly what Tim is proposing (IIUC):

https://github.com/apple/swift/blob/73841a643c087e854a2f62c7e073317bd43af310/docs/proposals/Accessors.rst#the-beacon-of-hope-for-a-user-friendly-future-inversion-of-control

This is an idea that makes sense to do at some point, definitely.

-Joe

···

On Oct 11, 2016, at 8:26 AM, Xiaodi Wu via swift-evolution <swift-evolution@swift.org> wrote:

The following document may inform your discussion here:
https://github.com/apple/swift/blob/73841a643c087e854a2f62c7e073317bd43af310/docs/proposals/Accessors.rst


(Xiaodi Wu) #10

The following document may inform your discussion here:
https://github.com/apple/swift/blob/73841a643c087e854a2f62c7e073317bd43af310/docs/proposals/Accessors.rst

···

On Tue, Oct 11, 2016 at 6:42 AM, Tim Vermeulen via swift-evolution < swift-evolution@swift.org> wrote:

Oh, I see. I’m not sure about the dictionary one, that probably depends on
whether the `Dictionary` type could use this addition to make member
mutations O(1). The other example probably wouldn’t benefit from replacing
the setter by a mutator, as there’s nothing to gain here - the
implementation is already optional. This is exactly why `set` shouldn’t be
removed from the language :slight_smile:

`mutate` wouldn’t be able to replace each and every setter, but there's a
lot of situations imaginable where it can.

On 11 Oct 2016, at 13:20, Jay Abbott <jay@abbott.me.uk> wrote:

In case c) for example if your set implementation was something like this:
set { _internalDict["propName"] = newValue }
set { if newValue { _int |= SOME_BIT } else { _int &= ~SOME_BIT } }
Could they be implemented as 'mutate'?

On Tue, 11 Oct 2016 at 12:05 Jay Abbott <jay@abbott.me.uk> wrote:

actually scratch point a) - that's bloody obvious :smiley:

On Tue, 11 Oct 2016 at 11:58 Jay Abbott <jay@abbott.me.uk> wrote:

This is interesting. I'm trying to evaluate your statement that "No
setter would be needed if a mutation clause is provided" but I can't think
exactly what the compiler would do in the case where there is a 'get' and
'mutate', but no 'set'...
a) when you call a non-mutating function;
b) when you assign a new value.
c) when get and set aren't implemented with a matching hidden var (i.e.
using a bit in an int var or storing in a dictionary).

On Tue, 11 Oct 2016 at 11:26 Tim Vermeulen via swift-evolution < >> swift-evolution@swift.org> wrote:

Just having getters and setters doesn’t allow this (unless the optimiser
is really smart about it). All the current API allows is grabbing whatever
the `get` clause returns, mutating it, and then passing it into the `set`
clause, whatever that does. The `set` clause might not do anything, or what
it does could be seemingly unrelated to the `get` clause, so it’s not a
trivial task to optimise this.

On 11 Oct 2016, at 06:35, Erica Sadun <erica@ericasadun.com> wrote:

On Oct 10, 2016, at 9:53 PM, Tim Vermeulen via swift-evolution < >> swift-evolution@swift.org> wrote:

There have been many instances where unexpected bad performance was
caused by the interplay between getters, setters, mutations and the
copy-on-write mechanism. For example:

struct Foo {
    private var _array: [Int] = [1, 2, 3, 4, 5]

    var array: [Int] {
        get { return _array }
        set { _array = newValue }
    }
}

var foo = Foo()
foo.array.append(6) // an O(n) operation

I propose a `mutate` clause which provides a `mutate` function with a
single inout parameter (similar to how `set` provides `newValue`), which
can be used instead of a setter:

var array: [Int] {
    get { return _array }
    mutate { mutate(&_array) }
}

The compiler could then translate each mutation of `foo.array` to a
closure with an inout parameter, which is then passed into the `mutate`
clause (which in turn is executed with `_array` as its argument, as per the
snippet above). For example, for `foo.array.append(6)`, the compiler would
internally generate the closure `{ (arr: inout [Int]) in arr.append(6) }`
and pass it into the `mutate` clause, `_array` is then passed as its
parameter and the array is updated in constant time.

I apologise if that was too hard to follow.

No setter would be needed if a mutation clause is provided, but I see no
good reason to do away with setters altogether, so this proposal would be
purely additive.

If this is computationally better, why is it not the default behavior
rather than an API change?

-- E

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

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