Optional explicit `self` parameter declaration in methods

I might still miss a bit where all this is heading so I'll ask probably a few more trivial questions:

  • What about variables (mutating computed variables)?
var something: SomeType {
  (self: inout Self) get { ... }
}
  • What about subscripts?
subscript (...) -> SomeType {
  (self: inout Self) get { ... }
}
3 Likes

Edit. crossing this out: ~ -1 as it feels like a step backwards to me. ~
I changed my mind and +1 on it now.

In the following example foo has a single argument:

struct S {
    func foo(Int) {}
    func bar() {
        foo(1)
        obj.foo(2)
        self.foo(3)
    }
}

even if we know that under the hood there's invisible self in there.

If there's a need to unify naming we could attempt to choose the name that works reasonably well in all contexts, example:

func foo(x: mut Int)   // was func foo(x: inout Int)
mut func(s: String)    // was mutating func foo(s: String)

i.e. use a neutral abbreviation like "mut" insead of full forms (mutating, mutable, etc)

Perhaps even this, as part of "great name unification" (unless it is one step too far):

    mut x = 1  // was `var x = 1`
1 Like

+1

Another idea.

@self(inout) func mutate() {...}

8 Likes

I think this reads very nicely, and the ā€œsugaredā€ version (i.e., how we spell the declaration currently) proceeds naturally from existing implicit self rules extended to the declaration site.

3 Likes

Using func mutate(self: inout Self) for this purpose is a Clever Trickā„¢, which I think in the end is a reason not to use it.

It formalizes (or perhaps ā€œblessesā€) the idea of an invisible first parameter, which is not currently part of the conceptual apparatus of instance functions. (Yes, I know that currying is possible, but no one is trying to sell currying as a way to understand the Swift language, AFAICT.) Itā€™s also redundant, leading to several suggestions already to remove the non-load-bearing part of the trick: : Self.

In regard to the dualism issue (e.g. mutating vs. inout), it might be convenient to decide to collapse all future pairs of this kind into a single keyword, but it leaves an unsolved problem. The mutating keyword applies to the function; the inout keyword parameter applies to a value (such as self). Collapsing these into a single keyword is just going to smear the concept in ways that donā€™t help anyone.

Putting aside brevity issues, one possible way forward is to forgo the function-specific keyword, and have just one keyword syntax that is explicit that it applies to self, say inout self. Forgoing the initial parameter ā€œtrickā€ too, then I think there are 3 possible spellings:

  1. inout self func mutate()

  2. func inout self mutate()

  3. func mutate() inout self

I donā€™t see much value in #2, and I just mention it for completeness.

#1 seems pretty natural, since weā€™re just trading a function-modifying keyword (mutating) for a keyword pair. The downside here is that weā€™re deepening the snowdrift of keywords piling up at the front of declarations.

#3 seems better to me, because weā€™ve already committed that piece of syntax real estate to keywords like throws and async that modify how the function behaves without writing out the behavior somewhere else in the declaration.

Note that Iā€™m not contemplating the briefer func mutate() inout, for the reason given above ā€” that this modifies the function not the self, and I think thatā€™s too ambiguous an approach. Also, Iā€™m aware that using 2 words where one would seem to do is going to be unpalatable to some. However, all of the suggestions made so far are unpalatable to some, so I donā€™t feel I need to hold back for that reason. :slight_smile:

1 Like

I think any suggestion along these lines should try to address Joe's point above:

Is there a particular issue with formalizing this in syntax somehow? I don't imagine this model is going to change.

Yeah... as much as I like the alignment between call site and definition site by putting the self before the method name I'm not thrilled about the keyword soup that it participates in. The originally-proposed version that sticks self in the usual parameter list has the advantage that it avoids this and also doesn't require paren-disambiguation.

If we go in the before-the-method-name direction at the very least I think we should require parens around inout self to avoid the appearance that they might be two separate keywords. Requiring the . as @Paul_Cantrell suggests also helps visually 'bind' the self to the method name itself rather than being part of the aforementioned keyword soup. And if we allow dropping : Self then the non-mutating variant becomes just:

func self.method(x: Int) { ... }

which IMO looks quite nice!

3 Likes

Subscripts already have parameter lists where self could live, but properties do not. I think something like get inout(self) { } feels the most familiar.

Hmmm, you all are going to talk me into my own idea.

Iā€™ll also note that this approach extends naturally to properties and subscripts if necessary:

func self.length: Int { ā€¦ }

func self.subscript(_ index: Int) { ā€¦ }

The mutating versions of both with separate get and set blocks donā€™t fall into place quite so naturally, however:

func (inout self).length: Int {  // Does inout belong here, even though
                                 // it doesnā€™t necessarily apply to get?
  get { ā€¦ }
  set { ā€¦ }
}

func length: Int {
  self.get { ā€¦ }
  (inout self).set { ā€¦ }  // Or does it move down here, even though that
                          // breaks the ā€œdeclaration mirrors call siteā€ rationale?
}
1 Like

Does it stand the YAGNI test? Assuming yes, there're plenty of places where we can stuff those modifies in. I'm not familiar with "constant-evaluable" so would be using "inout" as a placeholder:

inout func inout.method(x: inout Int) -> T {
}
// or
inout func method(x: inout Int) inout -> T {
}

to denote the case of the whole method being "blahblah" and also its "self" parameter being "blahblah" and also it so happens that it's "x" parameter is also "blahblah".

Would you ever need more than one modifier keyword for one thing? (again, if it passes the YAGNI test). If so - the syntax should be very different.

Yes. That was the point of my post (well, the first part of my post). In Swift language conceptual terms, the value on which a function is called is more than just a parameter being passed in. At the very least, it (well, its type) sets a sub-namespace for looking up the function name, which justifies its privileged position the the left of the function name, rather than to the right of it, as all the actual parameters are. It's also the provider of the function's behavior, again unlike all the actual parameters.

It's not a model. It's an implementation detail.

1 Like

As a user of Go, I find the Go function syntax very hard to work with. Specifically, it makes searching for the declaration of a function harder. Currently, you can search for ā€œfunc myFunctionNameā€ and be sure to find the declaration. If the Go syntax is adopted, developers will need a regex to find the declaration of an arbitrary function.

16 Likes

Consider dropping the "self:" part, kind of less noisy without it:

struct Foo {
  func method()
  // can also be written (but obviously there's no need):
  func method(Self)

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

I must admit the first parameter location is quite logical place to put attributes on self (especially if the same attributes can be independently applied to the function as a whole as well to mean a different thing).

1 Like

I'm not really a fan of this pitch. The syntax looks clunky and ugly and I don't find the motivating examples very convincing.

Can you elaborate on this? To me,

func change(value: mutating Int) { ... }

doesn't look any worse than

func change(value: inout Int) { ... }

. In fact, I'd argue that the former is better because it's more clear about how it's related to the mutating function modifier (something that has caused confusion before).

As for __consuming/__owned, wouldn't consuming/consumed or owning/owned work just fine?

Wouldn't it be more clear if these two separate concepts had two separate keywords?

func (const self).doSomething(with parameter: const Int) const { ... } // the meaning of const isn't very clear in each of its positions.

const could denote constant values (including self), and something like consteval could denote constant-evaluable functions.


If we really do need this, I think func self.method(x: Int) { ... } would be the best spelling.

2 Likes

What if you could express these as constraints on self using the where clause of a declaration?

func mutate() where self: const

Of course, this can lead to somewhat strange looking declarations:

func mutate() where Self: Something, self: const
3 Likes

I donā€™t think the proposal is to go full free floating methods like in go. In swift you would still need to put the method functions inside the body type or an extension.

1 Like

IMHO - no, that shouldn't be possible.

I don't think so. it's an extra noise and the method is not going to be called like other static methods anyway:

Foo.method(...) // Error: not a class or static function.
1 Like

Technically you need one already as spaces and/or newlines might appear between func and myFunctionName. If enough at-whatever appear before the "func" people in the real world may even do that.

Although really I think most people work in an IDE that has function navigation. I rarely edit Swift files with vi. I know that doesn't mean nobody does it (technically I just said I do it, but "rarely"), or that people don't occasionally try to navigate Swift source on a web site or something, but I'm pretty sure we are looking at an uncommon use case (non-IDE Swift nav) so that isn't a great reason to avoid something useful. It is a valid reason, just not strong enough on it's own at least.

1 Like

Computed properties also use mutating so the syntax we use for functions should also be portable there.

I think it should go in the same position as mutating/nonmutating perhaps as an attribute @selft(inout).

what about nonmutating ? @self(noninout), @self(let)?

@self(ref)?

1 Like

I've done this search as well, I think the ergonomic impact here is enough to override my aesthetic preference for a declaration syntax that mirrors the call-site syntax (unless we put self before the func but I don't really love that).

At least set does, and in fact also provides precedent for omitting a type annotation when the type is 'obvious':

struct S {
  var x: Int {
    get { 0 }
    set(myNewValue) {
      print(myNewValue)
    }
  }
}

IMO it would be reasonable to declare these with explicit self as:

struct S {
  var x: Int {
    get(self) { 0 }
    set(inout self, myNewValue) {
      print(myNewValue)
    }
  }
}

(though note below, I think these should warn since they're redundant)

nonmutating set is a good callout. I think it would be reasonable for the 'explicit self' definition syntax to imply the removal of all implicit modifiers on self, so if you wrote, say

var x: Int {
  get { ... }
  set(self) { ... }
}

then that would be the same as nonmutating set.

I think that we should probably also at least warn when someone has written a self declaration that's redundant with a modifier-free version of the declaration. So:

func myMethod(self) { ... }
var x: Int {
  get(self) { ... }
  set(inout self) { ... }
}

would all warn and tell the user to remove the self param. I don't think there should be multiple blessed ways to write a 'normal' method/getter/setter, and it would be better if the presence of an explicit self parameter actually indicated to the user that there was something special going on.

I think this is a why I prefer the func self.method() syntax as opposed to func method(self)(). Although both convey that the method takes self as something separate, the first one uses current implicit self rules. While it doesnā€™t create such a nice symmetry with partially applied functions Self.method(self)(), I think itā€™s the best especially for people who are new to programming and not really familiar with low-level function-signature transformations.

2 Likes