That’s interesting. It does solve some syntactic problems! I’m not sure the mental model fits perfectly well: although inout
does in part act as a constraint on what callers can pass in, it is not fundamentally a constraint. It doesn’t just subset allowed values; it changes the behavior of the function at the call site. The constraint model fits even less well for the hypothetical future borrow
and take
.
It's grown on me, I'm on board (perhaps with the mentioned syntax changes or without them).
The mental model is:
func foo(/*self: Self*/ x: Int) {
}
Here what's commented out is normally kind of collapsed, similar to how Xcode collapses the block of lines with a disclosure triangle on the left. And most of the time I don't care and don't want to see that. When I do care I "open the hood" and do the relevant changes. Hopefully won't be too often, like topping up oil in the car. And it is exactly where it actually is "under the hood".
Whether "mutating func foo" example qualifies here I am not totally sure, as it is so common place and is quite ergonomic in its current form. Continuing the car analogy it's like refuelling - you don't need opening bonnet for that". For me this is more like an advanced feature for the future borrow/take/whatnot attributes on self
.
I actually like self
-as-a-modifier much more than putting self
in the parameter list.
struct Foo {
self(borrow) func bar() { ... }
}
I’d actually be fine with that syntax. I think it’s better than func (borrow self).bar() { ... }
, even. self
is already a keyword and the idea applies to getter/setter pairs well:
var baz: Bool {
self get { ... }
self(inout) set { ... }
}
I really don't like func bar(borrow self) { ... }
syntax. self
is a special parameter that doesn’t belong in the argument list, so how does it belong first in the parameter list? Instance methods are meant to resemble a subject-verb-object structure, not verb-subject-object. And how would we explain unapplied instance methods having the type (Self) -> (...) -> ...
instead of (Self, ...) -> ...
?
Yeah I think I like the self(modifier)
syntax better than putting the whole thing in parens, but using that syntax we could have func self(borrow).bar() { ... }
(just to make sure we're making a fair comparison ).
Since this new explicit self
syntax would be opt-in, it could give us a chance to say that instance methods declared in this way have their unapplied forms uncurried, so that func bar(self, x: Int) {}
would have type (Self, Int) -> Void
.
What I dislike about this form is that you loose easy visual scanning and searchability by only searching for func bar()
.
I agree with this.
Adding the other alternatives really just confuses things.
FWIW I understand the problem this is trying to solve but I don’t like the suggested solution. Every time I work with classes in Python I lament this syntax.
I would find it more Swifty to have something like:
@self(inout) func mutate() {}
or just inout(self) func mutate() {}
, take(self) func consume() {}
, akin to private(set) internal var n: Int
.
Honestly, I am not sure that this change will be easier for new developers than learning mutating
keyword (if I understand the problem correctly). The complexity of this part of language, it seems, would be even harder, because you need to learn two equal ways of declaring method signatures, remember when you should avoid the first argument in method invocation, mix two different styles of signatures or make a lot of boilerplate, writing self: Self
for every method. Also, there is a question which variant should the compiler suggest when offering a fix for your code.
Don’t you think it could make the language even harder to learn and use?
I'm not sure you have understood the problem correctly? I don't think this is so much about the mutating func
syntax as such, but it is about its generalization: We'll soon have, not only mutating
, but a whole bunch of new ways to annotate how self
is behaving during a function call.
How can we scale the concept of mutating
while avoiding keyword soup, encourage gradual discovery, keep the "normal" cases lightweight? If we do add a new way of decorating or annotating self
in function/property/subscript definitions, it will probably mean that mutating
will become a synonym for some new future syntax.
. Like [Int]
is shorthand for Array<Int>
and
func foo<T: Proto>(x: T) -> R
is shorthand for
func foo<T>(x: T) -> R where T: Proto
can be
mutating func foo(x: Int)
shorthand for
func foo(self: inout Self, x: Int) /* or `mutable Self` if we rename `inout` */
although in the case of "mutating func" there may be a compiler warning suggesting using a shorter form.
We'll soon have, not only
mutating
, but a whole bunch of new ways to annotate howself
is behaving during a function call.
I agree that I probably do not see the whole situation and the benefits this pitch can give us. But what other behaviour can be added to self
? Could you please provide 2-3 more examples?
A couple of recent proposals add parameter modifiers that could be interesting to apply to self
:
Historically we've had trouble getting consensus on modifiers that "feel good" in both positions (hence the
mutating
vsinout
and__consuming
vs__owned
splits). It is also possible that an attribute or modifier might be equally applicable to the self parameter and to the method itself. "Constant evaluable" would be a good example of that: it might make sense to requireself
to be given a constant-evaluable argument, or for the entire method to be constant-evaluable.
If we can tolerate less than ideal names (can we?), we could use the same attributes for functions and parameters and it'd still be possible to use an attribute on both "self" and the whole function as the last two examples show.
func foo(x: mutable Int) // was inout. also, consider "mut"
mutable func bar() // was mutating
func foo(x: borrow Int)
borrow func var()
func foo(x: consume Int)
consume func bar()
func foo(x: const Int) // parameter is const
const func bar() // self is const
func baz(x: Int) const // applied on the function itself
// weird example but that's possible if needed:
const func foo(x: const Int) const // applied on all
borrow
and take
? Why should we apply them to self
when calling methods or accessing properties? I see the profit for arguments, but self
…
At this moment, it seems for me, we could better remove mutating
keyword from the language and, for instance, restrict mutating self
for structures in their methods. Because this word appears not only in method declarations, but also in protocols, it makes confusion when a class conforms to such a protocol, because suddenly you skip mutating
when implementing methods in classes. And it will also remove the difference between classes and structures in terms of possibility to reassign self
(it is also an example of inconsistent syntax, I think, that we can reassign self
in a structure, but not in a class). And then there will be no need of self
modifiers at all, and the language will become simpler.
because suddenly you skip
mutating
when implementing methods in classes
Class instance methods are always mutating, that's why we don't specify every method as mutating
. Swift currently can't express "nonmutating" methods in classes.
possibility to reassign
self
(it is also an example of inconsistent syntax, I think, that we can reassignself
in a structure, but not in a class)
It looks inconsistent superficially but these are of course two very different things (we could have even named them differently) self in a class is a reference (pointer), albeit its "pointerness" is hidden and you don't write self->field
like in C++, and self in a struct is the struct itself. Instead of assigning to self in a struct you can assign each field individually and it should have the same effect. And you can assign each field individually in a class as well - just its self
(reference) would remain unchanged.
Instead of assigning to self in a struct you can assign each field individually and it should have the same effect.
It should have, I agree, but it works slightly differently. In the example below, for instance, didSet
is going to be called only one time. The case is quite rare, but anyway, this moment exists.
struct A {
var x = 5 {
didSet {
print("Value is changed to \(x)")
}
}
mutating func changeByMutatingProperty(x: Int) {
self.x = x
}
mutating func changeByReassigningSelf(x: Int) {
self = .init(x: x)
}
}
var a = A()
a.changeByMutatingProperty(x: 10) // Triggers didSet
a.changeByReassigningSelf(x: 99) // Avoids didSet
This is one more reason why I believe we better keep only one self
behaviour, to make the code work and behave consistently. And it won't require adding explicit self
arguments, which also helps the language be easier to learn and read.
it works slightly differently. In the example below, for instance,
didSet
is going to be called only one time.
Well spotted. Technically reassigning self is not a change of its individual fields but I see your point, it can cause confusion.
Class instance methods are always mutating, that's why we don't specify every method as
mutating
This is not true: a mutating
protocol method can reassign self
(even when the conforming type is a class), which instance methods on classes cannot:
protocol P {
mutating func f(_ other: Self)
}
extension P {
mutating func f(_ other: Self) {
self = other // Fine.
}
}
class C {
func g(_ other: C) {
self = other // Error: cannot assign to value: 'self' is immutable
}
}
// Fine.
extension C: P { }
This discussion about class methods makes me realize there's an important difference between inout
and mutating
: mutating
protocol requirements disappear in a class context.
protocol P {
mutating func roll(dice: inout Int)
}
class C: P {
func roll(dice: inout Int) {}
}
With the new syntax, it would become:
protocol P {
func roll(self: inout Self, dice: inout Int)
}
class C: P {
func roll(self: Self, dice: inout Int) {}
}
It's a bit surprising to see inout
disappear like this, but this would reflect what happens with mutating
.
How should borrow
, take
, const
, or whatever else comes next behave for class self
?