Dot-prefixing considered ugly

While we are used to it, prefixing static members and enumeration constants with dots introduces visual noise, looks like a too clever hack and always amazes my colleagues from other languages' backgrounds:

func foo(_ v: Color) {...}
object.foo(.red) // 🤔

There's also this disturbing asymmetry between the absence of dot prefixes in declarations:

enum Color {
    case red
    case green
    static var blue = ...
}

and having those prefixes elsewhere:

switch color {
    case .red: break
    case .green: break
}
foo(.red)
foo(.blue)

Interestingly I can already use "object.foo(red)" in some contexts (e.g. when in a static method of Color) but not others.

I appreciate this would be a breaking change and no way we could possibly fit this change in swift at this stage. Forgetting that for a moment, do you think that Swift would have been better off or worse with the following change?

func foo(_ v: Color) {...}
...
object.foo(Color.red) // ok
object.foo(.red)      // 🛑 prohibited
object.foo(red)       // âś…

switch color {
    case .red: break  // 🛑 prohibited
    case red: break   // âś…
}
dispatchPrecondition(condition: .onQueue(.main)) // 🛑
dispatchPrecondition(condition: onQueue(main))   // âś…
bar(.init()) // 🛑
bar(init())  // âś…

// While choosing what "red" to use – follow these new resolution rules:
// - use local variable if exists
// - or use instance variable if exists (in which case self should be captured strongly explicitly or implicitly)
// - or use static variable if exists (in case of enum could be an enumeration constant)
// - or use global variable if exists
// - otherwise emit an error
1 Like

The implicit member syntax is one of my favorite features of Swift and I miss its equivalent dearly in the languages that require me to write out Color.red in equivalent positions. I also very much appreciate the visual distinction between "accessing in-scope identifier" and "accessing identifier from the scope based on the locally inferred type" that the leading dot provides.

89 Likes

I see the leading dot as quite the opposite of “noise”, from the perspective that “noise” implies that the character-count-to-information ratio is too high, but in this case of the leading dot that ratio seems to me almost as low as you can possibly get, because it’s one single character that indicates an extremely fundamental aspect of the code that’s it’s found within. (That fundamental aspect being that the value is a static member and not a local variable, which I believe can have a massive impact during local reasoning).

27 Likes

I actually don't think either of these look very good. This is just personal preference (and in a thread about what is "considered ugly", I think personal preference is fair game), but when constructing a value using an initialiser, I prefer to always write the name of the type and never use .init.

A static member like .red does a fairly good job of communicating to readers what it does, but .init() tells you basically nothing - it's the most generic member name in the language; almost every type has a member named .init. And if bar has multiple overloads, the one that gets called depends on what that .init happened to resolve to, which can be non-obvious to human readers and difficult to figure out using available tooling.

Static member syntax is a nice feature, but it's still worth exercising judgement when deciding where to apply it.

8 Likes

Perhaps that's because those other languages require you to write the full form Color.red instead of just red, and if they allowed the short form you'd not miss much?

Please note that with the above change you'd be able to write the short form foo(red) (as well as the full form foo(Color.red)).

That IDE feature could have been activated by other means, e.g. once you typed opening bracket, or once you typed = in the let a: Color =, or even via some explicit "suggest me" button.

Note that currently you can't immediately tell what red exactly is in foo(red): it could be local, instance or global variable. In fact it could be even static variable when you are in the static method (of Color in this example). Xcode could use a different highlighting depending on what that variable is, or reveal a tip when you hover over it, but other than that (e.g. in the plain text form) the difference is not immediately visible (which suggests it's not so fundamentally important). Or if it is...


... that line of reasoning could easily bring us to an opposite change where you will always be able telling the difference between local, static, global, instance variables, e.g:

self.red   // always use self for instance variables even when this is currently not required
.red       // always use . for static members even when this is currently not required
global.red // always use some prefix for global variables (bikeshed as "global.")
red        // when you see bare name that's always local variable, no exceptions

static func .foo() {...}
// in fact we can drop "static" as we have dot prefix, right?
func .foo() {...} // static func as it starts with dot


enum Color {
    case .red, .green
    static var .blue: Color = ... // as above we could drop static here
}

if .true { ... }

Which of the three methods you'd prefer, "no prefix dots", "current swift" or "always explicit prefix" (again, forgetting the restriction that we can't change swift so dramatically at this point)?

I believe that would be an error currently...

Yep :100:. Assume the question is for some Swift++ where we have no restrictions whatsoever.

1 Like

I'm not sure I can express how much I disagree with the OP :smile:

Honestly, dot prefixing of static members is one of my favorite features of the language, and one of the most distinctive, to me. Starting my typing with . in a context, and getting autocompletion about what I can write there is absolutely fantastic. In fact, to transform types into types I usually extend the destination with a new init so it shows up in autocompletion (in that scope).

More specifically, I think these comments are profoundly wrong:

object.foo(.red)      // 🛑 prohibited
object.foo(red)       // âś…

I read red as "a local variable called red", while .red as "constructor called red of the expected type in that context". I wouldn't dream of calling it "visual noise". The "asymmetry" between the name of the member and the way it looks at the usage site exists precisely to show that the latter is a static member of the type.

TBH, if you looked at mine and my team's code, you'd probably be very, very annoyed :sweat_smile:

25 Likes

I am not sure how can you disagree with a question :slight_smile:

That's exactly what I'd hate. Would prefer a.b.c.d to d(c(b(a)))) any day.

was that "let one" a global? In that case - no, static will be found first, assuming the change follows the rules:

There's plenty of statements in the original post (including the title) that one can disagree with, not just questions (which, given the statements, clearly imply a certain position), like:

About this

that's a matter of personal preference, but thinking in terms of constructors is objectively more discoverable.

Assuming that a: A, b: B, c: C and d: D, consider the following function:

func foo(_: D) {}

it requires a D, so one could call it like this:

foo(a.b.c.d)

but to do this, one must know that d is a member of c that is a member of b that is a member of a, so one must "think backwards" to get how to finally produce a d, and start with a.

Imagine instead that D has a .d(_: C) constructor, and C has a .c(_: B) constructor and so on. When calling foo(...) one must just write the . to be presented, by autocompletion, with the .d constructor. .d(...) would then require a C, a would then write the . to be presented with the .c constructor (in fact, one doesn't need to know that .d requires a C, it would be obvious from the autocompletion).

So, the function call would literally be automatically produced by autocompletion, like this:

foo(.d(.c(.b(.a))))

this would flow very naturally and effortlessly from autocompletion.

Now, this is a rather extreme example, but it shows that converting types with a static member of the target type allows to start thinking about what's required in a particular context, autocomplete that member, and then if the member requires something else recurse on the . syntax to "discover" the options.

10 Likes

Why do you prefer this? To me the choice between explicit and implicit feels like a useful architectural tool that lets me exert some influence over the "ways in which my code churns", so to speak. The concrete effect that I'm referring to is of course the fact that when it's implicit, changing the type name causes no churn.

I am definitely** in this camp.

** Update:

@ExFalsoQuodlibet 's post made me realize that there is more nuance here, I'm no longer "definitely in this camp".

Clearly, we need both static members and instance members. Combining them to make "good" code is non-trivial, and in some ways maybe even subjective.

Here's a made-up chunk of code that I could realistically write:

downloadVideoForOfflineUse(
    videoMetadata:
        responseData
            .jsonDecoded(as: FetchVideoMetadataRESTResponse.self)
            .asVideoMetadata(),
    downloadSettings:
        .init(
            useCellular: false,
            continueInBackground: true,
            completionNotification: .standardDownloadCompletionMessage
        )
)

It combines static members, initializers with implicit type (.init), instance members into a piece of code that I find to be a quite readable and which I also would experience a high level of discoverability-via-autocomplete while writing. Critiques?

Yes, this is true, but you do know something concrete about foo(.red), which is that red is a static member of the argument type, which more importantly implies that the value is not being influenced by the instance that is making the call, which is a useful piece of knowledge that goes completely out the window with foo(red).

I don't think you necessarily need static methods here, even for IDE autocomplete to work, for example once you typed "foo(" one of the autocomplete options could be "D()", once you autocomplete it to "foo(D(" IDE presents you with another autocomplete with "C()" as one of the options, etc, so at the end of the day you are getting autocompleted:

foo(D(C(B(a))))

which is arguably less noisy then the dot prefixed version:

foo(.d(.c(.b(a))))

Not to mention you won't need to have those static initialising methods to begin with – another source of noise – and if you do have those as statics and that's a class - subclass implementers would be very very unhappy.

Regardless, even the foo(D(C(B(a)))) version would feel backward to me (literally), with all those brackets, which in reality could be much worse due to function arguments:

foo(D(C(B(a), xxx()), options: Options(...))) 

I'd try to make it fluid-style till the very end if possible:

a.b.c.d.foo()

which wraps nicely for more complex expressions:

    someObject[1]
        .someMethod(param1, param2)
        .someOtherMethod(...)
        .yetAnotherThing
        .foo()

It might be important (and since you brought it up I'd like to know your opinion on the opposite change where you always know the variable scope, no ifs, no buts), would you prefer that change to the current swift version? If the difference between static and everything else is so important, perhaps as important is the difference between local, global and instance variables, and then the opposite change is what we'd like to have ideally (along with always requiring dot-prefixing for statics like in the examples in the quoted message)?

Two considerations that intend to show that the importance of knowing whether the variable is static or not (or, equally, whether this case is frequent enough in practice to bother about) is exaggerated:

  • remember (now dead) Hungarian notation. People advocating that also claimed "how else would we know the difference". In fact the current responses defending dot prefixes remind me Hungarian notation advocacy a lot.
  • Other languages (dozens of those) are quite happy without dot-prefixing static members (unless those that require full notation like C.foo). There are no attempts to borrow dot-prefixing from swift to other languages (while some other swift features – like optional chaining – were borrowed). If dot-prefixing was clearly superior it would have been copied to other languages.
1 Like
The Art of Programming

This topic suddenly feels to me analogous to a vaguer concept that I've thought about, which is about where to start when trying to achieve something in code. I have noticed that there are two quite different but equally valid places to start, which could be called "the top" and "the bottom". I basically bounce back and forth between the two, digging down from the top and stacking up from the bottom until the two tunnels run into each other. Then I refactor. I think that the debate about whether to use nested static members or chained instance members might be analogous to the debate about whether to start from the top or the bottom (the answer being "both", I think).

For example, imagine I'm making a budgeting/finance tracker app which distinguishes itself from other similar apps by a particular piece of the interface that makes an important aspect of the interaction much nicer.

On the one hand, we should start with that piece of the interface. That screen was the motivating idea - it's the thing we know most clearly that we want, and for that reason it's the code that we will be able to make least wrong. The rest of the code should be a consequence of that key screen. Start with what you want, and then flesh it out until it works.

This approach is analogous to static members, where "starting with what you want" is typing . and selecting a static member, and "flesh it out until it works" is filling in the placeholder types until it's a fully and properly formed expression.

I think that that approach can sound perfect in theory ("perfect" as in, the only way it should ever be done), but the reality is that when fleshing out downwards it can be unclear what is the proper way to continue, and sometimes it is possible to flesh it out in a way the turns out to be fundamentally incompatible with the low-level tools that your code is eventually going to have to make contact with and use.

Therefore, another valid starting point is "the bottom", i.e., start not with what you know you want, but rather with what you know you have, and then build it up from there. For example, imagine that we didn't have UserDefaults yet, but we did have the lower-level tools that comprise it. We basically know that for out budgeting app we're going to want to work with persistence at the level of abstraction of UserDefaults, so we can fairly confidently begin to stack up our tools in that direction without worrying that it will be fundamentally incompatible with where we want to end up.

This approach is analogous to using instance members, where you start with what you know you have (e.g., responseData) and then you progressively transform it into what you want.

This approach could also sound perfect on paper, but it suffers from the same fundamental problem, which is that sometimes the way we stack things up turns out to be fundamentally incompatible with the high-level outcome that we were aiming at.

Therefore, bouncing between the two seems to me like the only way, and even then it remains an art, not a science.

1 Like

A gentle reminder that this is a working list which exists for the purpose of developing ideas for Swift's actual future evolution. We have a lot going on that's actually possible to implement which could really benefit from community attention and commentary, and counterfactuals/thought exercises in language design for some language other than what Swift can actually be—while fun—are probably best reserved for some other forum.

10 Likes

while the scope of the original complaint is probably too broad to be useful, i think there is merit in discussing the drawbacks of leading-dot syntax - one of which is that it is utterly non-composable with result builders, because statements that begin with a leading dot parse as member references on the expression from the line above it.

8 Likes

I kind of like the dot prefixes, but I'm not a fan of the Enum syntax. I turn everything into English sentences in my head, so I think of case in switches to read "in the case of".

I'm not sure what the syntax gets us, really. (Not a fan of the keyword "switch", either but that's another thing entirely).

The dot prefixes tell me that we're looking at a member of a type, not a variable or constant, and it's simple and clean. It might not be symmetric with the declaration, but the usage isn't symmetric either, one being a binding and the other a lookup.

I don't find that it is true that changing a type name causes no churn; extensions, stored properties and enum payloads cannot always use inference, so if I change a type name, I'll almost always need to update other code to use the new name anyway (often using Xcode's refactor tool, so it's relatively painless).

Besides, I don't find that I change type names very often. Putting it all together, I feel that the decrease in readability isn't worth it. I have seen code that looks like .init(.init(.init(foo))), and I find that harder to reason about than if it had just said something like: Widget(String(Int(foo))).

That doesn't apply to all static members - as I said, a member named .red, for instance, clearly communicates that it is somewhat related to colours; .init just has so little information that I personally find it too opaque at the call site.

4 Likes

To be clear I was saying that when a particular initializer is implicit then that line will not churn if I change the type name, which reduces the overall churn.

Regarding readability:

Would you agree that my example is actually made more readable by the use of .init, because the type name would have just been redundant noise in the context of the parameter name?

A possible argument against this usage of .init that doesn’t have to do with readability is that in this case a change in type name could indicate a corresponding change that I should make to that argument label, which I would be alerted to if I have to churn and which I will miss if I don’t.

The thing is though that my type names are much more likely to change their phrasing or their clarifying additional words than their essence, which will often mean that argument labels such as the one in this example will continue to read perfectly even after the type name change (therefore, for now I continue to use .init in this way to what I think is good effect).

Lastly, those last two paragraphs made me realize that there’s actually one more argument for the positive readability of .init, which is that in examples like mine not only would the type name be redundant noise, in my code it would actually most often be an especially verbose and noisy name relative to the argument label (which I think is not just me, but rather a natural consequence of that types mostly share a global namespace while argument labels only have to be unambiguous within a much more restricted context).

With all of that said, yes I agree, this sucks.

Like I said, it's a matter of personal preference, but even in that example I don't feel .init makes up for the loss of clarity at the point of use:

// SomeFile.swift

struct SettingsA {
  var flag: Bool = false
}

struct SettingsB {
  var count: Int
}

func doTheThing(_: SettingsA) { print("A") }
func doTheThing(_: SettingsB) { print("B") }
// AnotherFile.swift

// Which does this call?
// Unclear unless I know a lot of details about the interfaces of SettingsA/B.

doTheThing(.init())

// What about these? Still not entirely obvious.
// There is much more required knowledge to understand what this does.

doTheThing(.init(flag: true))
doTheThing(.init(count: 42))

// And these? Much more obvious.

doTheThing(SettingsA())
doTheThing(SettingsA(flag: true))
doTheThing(SettingsB(count: 42))

By specifying the type name, I can have confidence that even people who are brand new to this codebase, and/or reading it using a non-sourcekit interface (e.g. GitHub), will immediately know exactly which type is being constructed, and which overload is being called.

IMO, anything that is not the most obvious thing needs good justification, and .init is almost never enough of a gain to make up for what we lose.

Also, I don't like the negative connotations of the word "churn", or the idea that minimising it should be a goal. My coding style is not designed to minimise the number of lines that need to be edited were some hypothetical future change to occur - it is designed to be clear to the people who actually read and maintain it now, as it is.

If I rename a type, it's because there's a new, better name for it, which better describes what it does. I do not mind at all if some code needs to be updated to use that better name.

Of course, I realise that not everybody feels the same way. It's a self-imposed rule to not use .init (or at least to use it very, very sparingly), but I feel that I can justify it and most people I've explained it to don't consider it an unreasonable position to take, even if they choose to continue using .init.

5 Likes

I somewhat agree with that train of thought. But is it unique to "init" though?

struct Rgb {
    static func red(density: Int) -> Rgb { ... }
}
struct Cmyk {
    static func red(darkMode: Bool = false) -> Cmyk { ... }
}
func doTheThing(_: Rgb) { ... }
func doTheThing(_: Cmyk) { ... }

....

doTheThing(.red())
doTheThing(.red(darkMode: true))
doTheThing(.red(density: 0.42))

Obviously the clash of "init" name is quite probable, but it could happen with static function names (e.g. "normal", "default", "custom", etc, or even less generic names like "red" in the above example).

usually when i write russian nesting inits it is because i actually meant Widget.init(foo), but the sugaring inits were not worth the additional boilerplate on Widget. if the interceding type conversions spelled out with Widget.init(String.init(Int.init(foo))) are not important or surprising i would prefer to read .init(.init(.init(foo))) because that communicates “this is just doing the obvious things to turn this Int into a Widget”.

here’s a common example i run into:

init?(parsing string:Substring)
{
    self.init(rawValue: .init(string))
}

self.init(rawValue: String.init(string)) doesn’t add anything meaningful, i am only copying the string because string-backed enums don’t know how to decode themselves from anything other than a String.

you did not label the doTheThing(_:) arguments, which signals that SettingsA and SettingsB are merely different shapes of the same concept. if it were important to distinguish between them at the call site, i probably would have labeled them doTheThing(a:) and doTheThing(b:).

among

only SettingsA is completely defaulted, which indicates to me that SettingsA.init(flag: false) is a sensible default value for the “doTheThing” operations. (if it’s not a globally sensible default value, then it shouldn’t be constructible with .init())

so the specific overload chosen by

should not be relevant on a first reading of this code.