Optional explicit `self` parameter declaration in methods

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.

1 Like

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.

1 Like

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, ...) -> ...?

4 Likes

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 :slightly_smiling_face:).

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.

1 Like

What I dislike about this form is that you loose easy visual scanning and searchability by only searching for func bar().

7 Likes

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.

1 Like

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?

4 Likes

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.

:100:. 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.

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:

2 Likes

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.

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.

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.

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.

Well spotted. Technically reassigning self is not a change of its individual fields but I see your point, it can cause confusion.

1 Like

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 { }

6 Likes

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?

1 Like