Allow argument labels to be trailing or preceding

A somewhat similar idea has been discussed elsewhere, but I'd like to approach it from a slightly different standpoint.

Even though the API Design Guidelines ask us to write method and function names that read as grammatical phrases, this is sometimes impossible due to the requirement that argument labels always come before the arguments they label. I've often wondered if it would be possible to allow function and method argument labels to be positioned as trailing if it makes more sense grammatically.

This comes up most frequently in delegate/observer/subscriber methods, where you have constructions like this:

protocol MyObjectDelegate: AnyObject
{
    func myObject(_ myObject: MyObject, aFooWasUpdated foo: Foo)
}

// in use...

delegate?.myObject(self, aFooWasUpdated: self.foos[i])

This type of construction violates the API Design Guidelines since it doesn't read smoothly, and also names the type of the Foo argument when there isn't a real need to. I would love to be able to write this as:

delegate?.myObject(self, self.foos[i] wasUpdated)

Which would have the signature:

func myObject(_ myObject, foo: Foo wasUpdated)

Has anyone else ever desired such a syntax?

I was only able to find the above thread while searching, so if there have been other discussions please point them out.

1 Like

I’ve actually thought about something like this multiple times, but I always come back to it being a very niche feature that could be confusing in use and I presume make the compiler more complicated for having to support it. So while sometimes nice, I don’t think it’s worth the effort.

1 Like

I run into situations where I'd like this feature every once in a while. Just yesterday I had a use case where I wrote two methods along the lines of this:

func insert(_ element: Element, at index: Int)
and
func insertAtSelection(_ element: Element)

It'd be much more symmetrical and in line with how Swift typically reads to be able to write func insert(_ element: Element, atSelection).

I don't know what all the implications of supporting this feature would be, but assuming there aren't major drawbacks I'd like to see it supported.

I think this would read better as:

insert(element: Element atSelection)

The commas then continue to provide clear grouping about which arguments are associated with which labels, and can't be mistaken for an additional argument which has no label.

2 Likes

I have long wanted something approximately like this, because I regularly have to write Swift code in four human languages besides English (German, French, Hebrew, Greek), and I have seen other projects which exclusively use at least four languages I do not speak (Portuguese, Chinese, Korean, Japanese).

Because Swift assumes English, the more a language’s grammar deviates from English, the more cumbersome it becomes to use. I have toyed with the idea of writing a broader localized code manifesto with additions that would make this easier, and circumfix function names and argument labels are at the top of the list. I have not actually taken the time to write out all my thoughts yet, because the last few Swift versions have been very focused on source and then ABI stability, so these sorts of additions would only have been deferred anyway. However, here are my thoughts on this one:

  1. Function names currently require an SVO or Subject‐Verb‐Object structure, which means that the action must come before the object it happens to. English generally works this way, but it is abhorrent to some languages. I believe it would be a purely additive change to introduce a trailing section to the function name:

    // English, SVO (Subject‐Verb‐Object)
    array.insert(newElement, at: index)
    
    // German, SOV (Subject‐Object‐Verb)
    feld.(unter: index, neuesElement)einfügen
    
    // ...where (parentheses indicate token positions)
    // array (1) = feld (1)
    // insert (2) = einfügen (5)
    // newElement (3) = neuesElement (4)
    // at (4) = unter (2)
    // index (5) = index (3)
    

    A function name would have a leading section, a trailing section or both:

    func leadingSection()trailingSection -> Void {}
    func leadingOnly() -> Void {}
    func ()trailingOnly -> Void {}
    
  2. Function labels currently assume prepositions, which means the relation is marked before the thing being related. Again, English mostly works this way, but it is abhorrent to the grammars of other languages. Labels could be abstracted to adpositions, which may be pre‐, post‐ or circumpositions:

    // English, prepositional
    array.insert(newElement, at: index)
    
    // Turkish, postpositional
    dizi.(dizin :e, yeniÖğe)eklemek
    
    // array (1) = dizi (1)
    // insert (2) = eklemek (5)
    // newElement (3) = yeniÖğe (4)
    // at (4) = ‐e (3)
    // index (5) = dizin (2)
    

    An argument label would have a leading section, a trailing section or both:

    func name(
        leadingSection: argumentOne :trailingSection,
        leadingOnly: argumentTwo,
        argumentThree :trailingOnly
        ) {}
    

These same things would also be available for English for cases such as your examples:

func myObject(_ myObject: MyObject, _ foo: Foo :wasUpdated) {}
// ↓
delegate?.myObject(self, self.foos[i] :wasUpdated)

func insert(_ element: Element)atSelection {}
// ↓
x.insert(element)atSelection

Some more technical details I have already considered:

  1. The circumfix arrangement of the name does not have to live past the early stages of the compilation process. Name mangling and such could simply concatenate the two halves with some joiner and treat it as a single name, as long as the joiner is one that is not currently legal in an identifier. A joiner somewhere in the middle would indicate where the break is between the two halves. A joiner at the beginning would indicate no leading half, and no joiner at all would mean no trailing half (which describes everything we currently have).

  2. Selector‐like syntax could use a space to split labels. Trailing‐only labels could use _ to mark the absence of a leading label, in order to disambiguate from an identical leading‐only label. Wherever unambiguous, it could be optionally left out.

    let method : (Int, Int, Int) -> Void
        = MyType.foo(leading trailing:leadingOnly:_ trailingOnly:)
    

    In order to be used from Objective C, methods involving trailing identifiers would need to be given a separate selector name with @objc(someOtherName), since Objective C does not support circumfix function names or argument labels.

3 Likes

This is pretty cool, and feels like the logical next step to supporting Unicode in identifiers.

I avoided the colon in the trailing section since I assume it introduces a pretty severe ambiguity in the language (func foo(argumentOne :trailingSection) and func foo(leadingSection: argumentOne) are syntactically identical unless we make the whitespace significant). Someone more familiar with the parser could comment more specifically, but AFAIK the func foo(argumentOne trailingSection) doesn't introduce such an issue since whitespace-juxtaposed expressions are not allowed.

I suppose something might arise if there were a postfix operator that ended an argument expression which also happened to be an infix operator, and the argument label was also an in-scope identifier with the proper type, but that seems like it would be exceedingly rare.

Good catch.

I think that would only affect poorly written code, and even then very rarely:

  1. I have seen no style guide which recommends anything but no space before the colon and some whitespace after it (at least related to function parameters/arguments). That means the compiler could default to the same whitespace heuristic it currently uses to guess at prefix, postfix and infix operators, and then fall back to the opposite only if the fast path fails:

    1. x: ylabel: argument
    2. x :yargument :label
    3. x:y → ??? → Try 1 first, since it is the only variant that was allowed ≤ Swift 5.
  2. Given that labels are normally adposition‐like phrases and variable names are generally noun‐like phrases, only one of the two arrangements is likely to ever actually match declared identifiers. In the extremely rare case that it is still ambiguous, an extra colon could be allowed in order to enclose the argument from both sides even when there is no label to make it ultra‐explicit: (w, : x :y, z) and (w, x: y :, z) would be the two unambiguous versions of (w, x:y, z). Alternatively an underscore could be required too to blank out the absent label: (w, _: x :y, z) or (w, x: y :_, z).


Your spaced version is a reasonable alternative, I just worry that the visual cue the colon provides would be lost in situations (like Turkish) where almost everything only has postposition labels.

Given:

typealias Dizi = Array
extension Dizi {
    typealias Dizin = Index
    typealias Öğe = Element
}
var dizi = ["a", "b", "c"]

Which is better?

extension Dizi {
    func (_ dizin: Dizin e, _ yeniÖğe: Öğe)eklemek {
        insert(yeniÖğe, at: dizin)
    }
}

dizi.(3 e, "ç")eklemek

vs

extension Dizi {
    func (_ dizin: Dizin :e, _ yeniÖğe: Öğe)eklemek {
        insert(yeniÖğe, at: dizin)
    }
}

dizi.(3 :e, "ç")eklemek // This is an exact reversal of the English.

At the same time, leading colons as a punctuation symbol do not have a precedent in any human language I am aware of, so a more logical replacement would certainly be welcome. A hyphen might be a suggestion, since many postpositions are actually suffixes:

extension Dizi {
    func (_ dizin: Dizin ‐e, _ yeniÖğe: Öğe)eklemek {
        insert(yeniÖğe, at: dizin)
    }
}

dizi.(3 ‐e, "ç")eklemek

A hyphen (U+2010) is not valid yet anywhere in the grammar, so it would be very viable. The downside is it would be the first time Swift required a character from outside ASCII and it would be visually mistakable for the minus operator, at least without syntax highlighting.


P.S. In typing these last examples I noticed that there can already be whitespace‐juxtaposed, collapsable tokens in the declaration when the label is different from the name, though it might not matter, since the type has to intervene before the trailing section:

func doSomething(object anObject: AnyObject) {}

(Interpreting those two identifiers with SwiftSyntax is currently a nuisance. The compiler only gives you firstName and secondName and have to sort out labels vs names yourself.)

The difference between this and the operator system is that the differentiation between prefix/postfix/infix occurs at lex-time (see Lexer::lexOperatorIdentifier()), and the system you describe wouldn't be able to fully resolve until after parsing, at which point we would be rewriting the AST... Unless we wanted to explicitly disallow the argument: postfixLabel and prefixLabel :argument forms, I think this would likely introduce a decent amount of complexity. (Note: I'd prefer the leading colon form if it were feasible, so would love to hear from anyone who could contradict this).

Without speaking Turkish it's a bit hard to say, but point taken :slight_smile:. Like I said above, I'd prefer the leading colon if it doesn't introduce too much complexity. I really don't like the hyphen for both of the reasons you mentioned, but also because it doesn't really signify a divider at all in my mind.

Naming method references would become a bit troublesome using the whitespace-only version, but if we only allowed an argument to have either a prefix or a postfix label, and not both, then the current system could be maintained.

Yeah, it would be nice to know if there is anyone out there right now who deliberately writes label :argument in Swift 4. It would tell us whether disallowing it would be a real source‐breaking change or just a theoretical source‐breaking change. Either way it would be a trivial for the migrator to fix automatically. Not having to deal with it would make things significantly simpler.

1 Like

-1000

This will undoubtedly make code harder to read.

5 Likes

The entire point of this discussion is that readability is biased by the native language of the programmer. Such code would be harder for you to read, and likely for me to read. However, that doesn't mean that Swift should be limited to those whose native languages are similar to English in sentence structure.

There's an argument to be made that Swift has to be opinionated, and Swift is definitely not Perl. But options are not always a bad thing.

2 Likes

I’m not a native English speaker, and my opinion is informed by this very fact. As a non-English native speaker, I don’t want to encourage witting code in more natural languages with more complex syntax.

7 Likes

A few things I noticed:

  • because of between statement commas, (I think) there is no ambiguity in:
    func doSomething(prefix internal: Int :postfix, next: Int) {}
    x.doSomething(prefix: value :postfix, next: value)

  • However, having postfix function name parts is ambiguous with throws
    func doSomething(prefix internal: Int)postfix {}
    func doSomething(prefix internal: Int)throws {}

While there may be other ambiguities with the trailing name syntax, this isn't one of them (at least, not a true grammatical ambiguity). The compiler is capable of distinguishing between keywords and identifiers, allowing it to reject the following:

func throws() { // Error: Keyword 'throws' cannot be used as an identifier here
    print("throws")
}

The guidelines prefer grammatical phrases but allow for reasonable degradation in certain circumstances. That said, I think the example you give should be rewritten from

func myObject(_ myObject: MyObject, aFooWasUpdated foo: Foo)

to

func myObject(_ myObject: MyObject, didUpdate foo: Foo)

which would fulfill the desire for grammatical phrasing.

2 Likes

That makes a grammatical phrase, but slightly changes the meaning. It may not be the case that myObject is the one performing the update, but instead is observing and notifying the delegate of any changes to the Foos.

In that case, I'd suggest

func myObject(_ myObject: MyObject, didObserveUpdateTo foo: Foo)

Yeah, maybe there are better ways to phrase that specific example to make the phrase grammatical, but there are other examples in this thread that I still think are motivating. Prepositional phrases and non-SVO languages (as @SDGGiesbrecht pointed out) are both served by a reordering of labels and arguments.

Swift isn’t some language construct that’s generic over any natural language. It’s a formal language, partly influenced by English, as well as a handful of other programming languages.

To try to twist it into fitting with function and variable names in any language, is hindering readability, not helping it. Especially when it must be extended to allow all kinds of syntax informed by an array of spoken human languages.

This whole idea is ill-informed and flawed and it will hurt the Swift open source community while at the same time increasing the syntax drastically.

Let’s kill this idea right away!

(PS: I’m not a English native user)

11 Likes

I also felt that I wanted to change the language to improve readability when I wanted the Elvis operator gone ?: . I can see where you are going with this suggestion but I don't think it would improve the language. Labels in swift are already weird and no many languages have argument labels so I think its better if we keep them terse so they are easy to understand and explain.

https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20151214/002609.html

1 Like