Optional explicit `self` parameter declaration in methods

Many of the ideas we've been developing for ownership and performance control involve adding new parameter modifiers and/or attributes. Most of these could also apply orthogonally to the self parameter of methods, but because the self parameter is implicit, there's nowhere to place these modifiers using the normal syntax. Up to this point, we've defined alternative method-level modifiers and attributes to apply to self, such as using mutating func to mean that self is inout. This approach doesn't scale very well, because developers have to remember two modifiers, and the different spelling can give the mistaken impression that the effect is different.

We could take a cue from many other languages and allow the self parameter to optionally be declared explicitly as part of a method's signature, where it can be used with normal parameter modifiers.

struct Foo {
  func method()
  // can also be written:
  func method(self: Self)

  mutating func mutate()
  // can also be written as:
  func mutate(self: inout Self)
}

If it's present, I'd say that self should appear as the first parameter in the list, and its type must be Self (so this isn't an alternative way of declaring constrained extensions). Modifiers and attributes can be applied to self the same way they are to other parameters (unless they don't make sense to apply to self, in which case they can still raise an error). I think this approach will let us grow the number of controls we have on parameter behavior without having to also maintain a parallel universe of self modifiers. What do you all think?

17 Likes

Why not just allow the self modifiers to be applied to the function directly?

Historically we've had trouble getting consensus on modifiers that "feel good" in both positions (hence the mutating vs inout 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 require self to be given a constant-evaluable argument, or for the entire method to be constant-evaluable.

7 Likes

This sounds good to me. I've liked the results of this feature in C++, although I've only played around with it a little because C++23 isn't out yet. I feel like mutating func foo() being literal sugar for func foo(self: inout Self) will make it easier to explain how exclusivity checks on members work.

Most of the new modifiers for self parameters are likely to be niche anyway, so the lack of a sugared prefix form is unlikely to be a problem anyway. And in cases where it does become an issue, sugared forms can still be added.

3 Likes

Would it be permitted to call a method explicitly in its desugared form? And (regardless of the answer to that) should the desugared version be static func method(self: Self)?

6 Likes

I have mixed feelings. Teaching a semester of intro CS in Python last year, I found the language’s required explicit self was (contrary to popular Pythonic opinion) a source of great vexation: functions declared with n parameters take n-1 parameters when called. Students understandably had ongoing trouble getting their heads around this; it really does not make sense. Having an optional explicit self seems likely to only exacerbate that confusion.

That said, the rationale here is highly compelling. If there are modifiers that apply to parameters and could also apply to self, where else should they go? Surely making self look like a parameter for that purpose is preferable to novel syntax involving the whole function declaration.

In principle, it would be nice if there were some syntax that made self look at the declaration site the same way it looks at the usage site:

  • preceding the function name and
  • separated by a dot.

The result of that line of thought is…not instantly appealing to me:

// If it’s called like this…
widget.frongle(with: baz)

// …should it be declared like this? 😕
func self: Self.frongle(with x: Doodad)

// Do parens help? Or make it worse?
func (self: Self).frongle(with x: Doodad)

This smells funny to me, but it’s hard to see past my own familiarity bias judging it. Maybe I could see a case for the last option.

13 Likes

This seems mostly reasonable to me, and I'm a fan of a solution to the problem of coming up with adjective pairs that attach elegantly to both a parameter and a method.

A couple thoughts:

  • Do we want to call out the currying behavior of self explicitly with whatever syntax we chose? It's not formally the case that self is just another parameter in the list, so I could imagine we might want to instead write this as:
    func mutate(self: inout Self)(otherArg: Int) -> String { ... }
    
    or similar.
  • self will always have type Self, right? Could we drop the type annotation (a la Rust), so that we'd write:
    func mutate(inout self) { ... }
    
    ?
    This loses the symmetry of attaching inout to the type, I suppose, but it also seems like always requiring self: Self in addition to whatever modifiers the user wants to apply is fairly noisy.
4 Likes

Riffing on this, and combining with my 'do we need Self?', what about:

func self.frongle(with x: Doodad)
func (inout self).frongle(with x: Doodad)
// or maybe just
func inout self.frongle(with x: Doodad)
3 Likes
An elaboration that doesn’t really make sense because I forgot a syntax change years and years ago

If it’s written as static func method(self: Self)(/* other args */), then your first question is already answered, since instance methods can already be called as if they were written this way. This also feels like a relatively-not-bad variant of Paul’s direction.

The idea isn't bad, but I have a few questions:

  1. How do you call the function?

A: myFoo.mutate()
or
B: myFoo.mutate(self: &myFoo)

Neither of these seem attractive. A is different from the definition, and B is clunky.

  1. Do you need to explicitly write out self within the method body?

  2. If the answer above is B, can't we just allow

static func mutate(self: inout Self)

  1. Since we already have mutating/inout, is the idea to eventually get rid of that, or have both version for mutating, but only one version for any future cases?
1 Like

I don't see a need to change the call site syntax. There are other languages, including Rust, Python, C#, and C++23, where the argument is not included in the call site even when self or this is spelled out in the method declaration.

3 Likes

func mutate(self: inout Self) {

// do I need to use self explicitly here?

}

I thought the only reason we even still had the currying behaviour for methods was because the attempted implementation to remove it missed the cutoff date for breaking changes. It would be sad if Swift 6 rolled around without fixing partial application of methods. I certainly wouldn't want to undo SE-0002 over this regardless.

I think self should behave the same inside the method definition as it would normally. If you need to retrofit parameter attributes to self in an existing method, it shouldn't break the source of the rest of the method in irrelevant ways.

2 Likes

What about reference types like classes?

class Z {
  // error: Covariant 'Self' or 'Self?' can only appear
  // as the type of a property, subscript or method result;
  // did you mean 'Z'?
  func foo(`self`: Self) {}
}

self is the very parameter that a method call is variant against, so it doesn't need to be held to that constraint.

7 Likes

Seconded. And having two syntaxes to define the same function, one with and one without the implicit/explicit self parameter, makes it even worse than having to explicitly add the self parameter for all functions taking self, IMO.

This would be a lot less confusing:

mutating func mutate()
// can also be written as:
inout func mutate()

Sure, mutating sounds better, but if you already know mutating is the same thing as inout, this is pretty self-explanatory. It's also much less of a syntax soup.

7 Likes

Another variation on the idea might be to use something like Go's syntax, where the "self" argument is declared before the method name:

func (self: inout Self) mutate() {...}
11 Likes

FWIW, in Python, self is always required at the call site (to the left of the dot, but always required).

AFAIK, Ruby and C# do not support an explicit self parameter in the declaration. (Or do they? If so, I completely missed that!)

None of that is to disagree with Joe’s conclusion:

Interesting, I didn’t know Go did that. Maybe that does create some precedent after all for the last option from my proposal to make the declaration look like the call site:

1 Like