Optional explicit `self` parameter declaration in methods

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

inout does not disappear on a class method, because the actual value being passed is the reference to the object, and class methods are never allowed to rebind self. This is somewhat of an artificial constraint—we could in theory allow mutating methods on classes, and those could change the self binding—but that would more likely than not be confusing, since class methods generally want to mutate the current object without affecting the value of the self parameter itself. borrow and take on the other hand do make sense for self on class methods, and they would affect the reference-counting convention used to pass the self reference.

3 Likes

This can be demonstrated with the use of default implementations.

protocol P {
    init(value: Int)
    var value: Int { get }
    mutating func increment()
}

extension P {
    mutating func increment() {
        self = Self(value: value + 1)
    }
}

final class C: P {
    let value: Int
    init(value: Int) {
        self.value = value
    }
}

do {
    var x = C(value: 0)
    x.increment()
    x.increment()
    print("C(value: \(x.value))") // prints: C(value: 2)
}

C.increment is a mutating function, even though C is a class.

Wouldn't it be confusing to have two different ways of expressing the same thing? Shouldn't we have only one way of expressing that a function can mutate a value type?

I'm not against func mutate(self: inout Self) to get rid of the extra keyword mutating and streamline things to just inout. But that doesn't seem to be your goal here, so I'm confused about how this is going to make things easier.

Sounds like things are more complicated with multiple options to express the same thing – without any of the two options being clearly shorter (like with [Int] vs Array<Int>) so I don't see any of the two alternatives as "syntactic sugar". Am I missing something?