language icon indicating copy to clipboard operation
language copied to clipboard

Dot shorthands: Could `.new(...)` be simplified to `.(...)`?

Open rrousselGit opened this issue 7 months ago • 7 comments

Cf the title.
I've enabled the experiment, and it seems that the syntax to invoke a default constructor is .new(...).

That seems a bit lengthy. I get that this is similar to constructor tear-off like const foo = Foo.new. But at the same time in this specific use-case, we loose quite a lot of the value of dot shorthands.

We end-up with either:

  • `function(Foo())
  • function(.new())

While .new may win when relying on really long class names, the average class probably won't benefit much.

I feel like function(.())would make the feature more appealing. The leading.` is already a context clue about the fact that we're relying on dot shorthands.

rrousselGit avatar May 02 '25 20:05 rrousselGit

Some counterpoints.

  1. .new is 4 characters, is it really that lengthy? I doubt very many type names are 3 or fewer characters.

  2. .() is a little inconsistent because you can't write Foo.(), but you can write Foo.new().

  3. .() can be a bit cryptic looking.

  4. Even if .new is longer in some cases, it can still reduce the amount of redundancy, which arguably reads better. Often the name of the function or the name of the named parameter includes the name of the type already. For example:

    • x.foo = Foo() vs x.foo = .new()
    • MyWidget(foo: Foo()) vs MyWidget(foo: .new())
    • Bar.fromFoo(Foo()) vs Bar.fromFoo(.new())

mmcdon20 avatar May 03 '25 00:05 mmcdon20

The point is about how if you factor-in autocompletion, the value of .new() is very low.
Typical types are slightly longer than .new, but they are also slightly easier when you consider that you don't need ..

Consider this:

class Foo {}
void fn(Foo foo) {}

If you type:

fn(

Autocompletion will naturally suggest Foo() as the first option (because it knows the type, so it's an obvious first choice).
So .new() is actually harder to type than Foo(), since Foo() just requires a single tab press to accept. But .new() requires at minimum typing .

I'd expect .new to have low usage ; even in cases where its use seems relevant.

.() is a little inconsistent because you can't write Foo.(), but you can write Foo.new().

This sort of thing is not unheard off.
Take function?.call() vs function!() :shrug:

And we have records now.
Records use (42,) not new(42)

.() can be a bit cryptic looking.

So is .new().
They are equally cryptic IMO.

.() just feels like a throwback to records.

For starter, there are other type-inference feature proposals which include similar patterns. Cf:

MyClass a;
switch (a) {
  case _(): 
}

The proposal isn't about _.new() but just _(). So .() feels logical.

In _(), _ refers to "Type from the switch context" In .(), . refers to "Type from the statement context"

rrousselGit avatar May 03 '25 00:05 rrousselGit

the code should be clear ,not short as possible .

i also dislike the un-typed/dynamic nature of InheritedWidget .

i think @mraleph has better taste in prog.-lang.s

atototenten avatar May 03 '25 11:05 atototenten

If there was a special case shorthand for Foo() I would prefer () if possible (no leading .). This would be consistent with just dropping the type name when you have an available context type. I suspect it would not cause any conflicts with records because you need a context type in order to use the shorthand, but I'm not 100% sure.

See for example:

Name example = (first: 'Abraham', last: 'Lincoln');

class Name {
  String first;
  String last;
  Name({required this.first, required this.last});
}

mmcdon20 avatar May 03 '25 18:05 mmcdon20

I actually dont like .()

Reason being, I see the dot syntax .new() as being actually _.new(), where _ is the context type, which can be omitted for brevity.

the dot remains the same as it's always been.

if we really wanted additional brevity, we could do new() without the dot, but a special case like this seems a bit far. i know some languages do this, but consistency is better I think.

plus, I actually like .new() over Foo() because we don't need to know the type name, even if its technically more characters.

.() just doesn't make sense. it reads weirdly, and its almost the empty record (unit) () which may lead to confusion, especially when groking (quickly scanning) code.

TekExplorer avatar May 14 '25 18:05 TekExplorer

if we really wanted additional brevity, we could do new() without the dot, but a special case like this seems a bit far

It works in C#, I quite like it. It wouldn't be a special case of shorthands either, it would just be a feature unrelated to the dot short hands

Maybe this could be suggested when pressing TAB instead of the actual class ? not sure if that's a good idea, it's quite context dependent whether you want to use new() or the actual class name.

Maybe some rules can be made such as that new() is the default autocomplete for named properties

cedvdb avatar May 20 '25 21:05 cedvdb

i think that new() is just not a good fit for dart, since we'd have .new() anyway. it would be redundant and confusing. People would wonder what the difference is, and there just isn't any. we'd end up with another thing to choose between and fight over. i just think that for Dart its not worth it.

new() would be a special case by definition. it would basically be a global function that's different based on context, semantically speaking - so its super out of place.

TekExplorer avatar May 21 '25 00:05 TekExplorer

the code should be clear ,not short as possible

I agree this is the goal, but clear for whom? Also they are not mutually exclusive, shorter can be clearer especially when it encodes the same meaning in a more concise form ie it is clearer and shorter to write out the product of a and b as a*b instead of a times b or in assembly-like languages mul a, b instead of multiply a, b. In isolation it isn't a big deal, but if you are read or even just skim large chunks of code at once the noise adds up


I think code bases set the expectation for what is clear/complex, and what is not. Example code for a newcomer should not use advanced syntax and features. However, a code base regularly maintained by people who are more experienced or at least willing to use more concise syntax should not be limited by that. It is all relative

Looking at the current feature-set Dart has already gone down the path of allowing for all sorts of ways of expressing the same functionality. It is up to the coders, with the help of lint rules, to decide what subset they want to stick to, if they care for these limits

I say conciser syntax forms for common patterns, such as @rrousselGit's suggested .() are useful and should be possible. It doesn't have to be encouraged to be used, and in fact, can be discouraged by standard lints if many are not happy with it. Let those who are willing to use the feature use it because there is still notable utility in it for them

RohitSaily avatar Jun 24 '25 15:06 RohitSaily

I disagree. There comes a point where we go past being concice and get too close to ambiguity.

A period is a rather tiny character. Without it, we have a record instead, which can cause significant confusion, and makes grok-ing harder, because now you have to hunt for that dot.

It's too far. Right now, the dot shorthand merely omits the target type. By that logic, .() doesn't make sense. It's a special case - a unique syntax that doesn't really follow any existing rules.

We do not want this feature to go from a useful shorthand to a detrimental trap.

We also shouldn't create options where it's not necessary - dart is often opinionated (such as the formatter) so it makes sense to keep it simple.

Your talk of advanced users doesn't make sense either, because they'd just use .new() which is plenty short, and easily auto-completed by just typing . + tab. (Presumably we prefer placing .new at the top in IDEs)

I really don't want to have to disambiguate records and dot-short constructors, and I don't think any of you want that either.

Seriously, it's a trap - and not a valuable one.

The proposal isn't about _.new() but just _(). So .() feels logical.

No, not really. I see the proposal as such: _ is the context type. _.new() is a simple way of calling something on the type .new() is the same, but omits the underscore, which is now implied.

To cut it down to .() doesn't make sense, because we'd actually want is _()

.() can be a bit cryptic looking.

So is .new(). They are equally cryptic IMO.

No, they arent. .new() is clearly a static access on a context type (as long as you know about shorthands, but even then you can guess)

While .() Just... What? What is that? What does that even do? we don't even allow passing callable objects as functions - you have to tear off .call, so why are we doing... Whatever this is?

It's too far, and encroaches on records. We need to not add syntax that's too similar, or it will cause confusion. I already hate it and it's just a proposal.

I get the desire for short code, but we need to make sure that the code still says what it does. Explicit is better than implicit - and while we're willing to make the type implicit, we should keep the actual static call explicit. We really don't need additional confusing syntax.

And for the record, no. My Ide does NOT let me use tab to autofill a default constructor with the type. It is much more useful for me to be able to use dot, because that actually has way more useful options attached to it for autocomplete.

TekExplorer avatar Jun 24 '25 17:06 TekExplorer

I think that's exagerrated. As I mentioned before, there are a bunch of related features with a similar syntax, like:

switch (person) {
  case _(:final name)
}

Instead of:

switch (person) {
  case Person(:final name)
}

Nowadays, the new keyword is pretty much unused outside of tear-offs.

I wouldn't be surprised if some of the younger Dart devs didn't even know that you could do new Class(). So by that standard, new() isn't any more familiar than .().

rrousselGit avatar Jun 24 '25 17:06 rrousselGit

I think that's exagerrated. As I mentioned before, there are a bunch of related features with a similar syntax, like:

switch (person) {
  case _(:final name)
}

Instead of:

switch (person) {
  case Person(:final name)
}

I don't consider that "advanced". _ is blank. Even a new dev can assume it means "anything" (or, perhaps, "fill in with" the context type itself, it's the same either way)

Especially when they learn that _ is non binding in most places.

Nowadays, the new keyword is pretty much unused outside of tear-offs.

I wouldn't be surprised if some of the younger Dart devs didn't even know that you could do new Class(). So by that standard, new() isn't any more familiar than .().

Who said anything about familiarity? It's about readability. .new() is already part of the language. Anyone who sees it will either understand it from tear offs (which is automatically preferred with a specific lint) or will see it in autocomplete and learn what it means from code docs and go-to-definition. That or they understand it from prior languages or basic dart language docs reading.

.() Is just a nothing burger that means the same thing but now you need to learn both what a dot shorthand is, but also what the heck this specific syntax that appears nowhere else even means. And it's easy to forget.

Plus, it may look similar to _ in switch statements, but it couldn't be more different. _ is an identifier - a direct replacement for the type, while . Is an accessor. In reality, it means _. and we just omit the _. They are NOT the same.

I don't think I'm exaggerating at all. It's a syntax that does not share syntax rules with... Anything else. It's a really weird exception.

And that's not even mentioning what I already said about record ambiguity. Not everyone has such sharp eyes to notice a tiny character. We dont always have empty constructors. Needing to process the unique sytax and noting that it is not a record takes away from the supposed quick typing.

TekExplorer avatar Jun 24 '25 17:06 TekExplorer

I disagree. There comes a point where we go past being concice and get too close to ambiguity.

This is a valid concern eg shortening things in nonconventional ways, such as this proposal. On the other hand, someone needs to begin the convention, and the nonconventional way can be opt-in and not imposed as standard.

A period is a rather tiny character. Without it, we have a record instead, which can cause significant confusion, and makes grok-ing harder, because now you have to hunt for that dot.

Again, I agree this can be the case for a significant number of readers. But this can be said for dot-shorthands in general. eg you may have copy as a static function but also use copy as a local name of an actual copy, the only differences in the usages can be copy and .copy. I also disagree with the idea that period is hard to visually identify, if that is the case the problem is the font size for the user and not the fact that a period is used. If it were so hard to visually identify it would not be such a frequently used symbol at all. In addition, this is with monospaced fonts, making them especially easy to identify

We also shouldn't create options where it's not necessary - dart is often opinionated (such as the formatter) so it makes sense to keep it simple.

There exists a standard formatting, but it also permissible to deviate from that and configure your own formatting. In fact, built-in linter rules (or lack of using them) encourage custom formatting

Your talk of advanced users doesn't make sense either, because they'd just use .new() which is plenty short, and easily auto-completed by just typing . + down + enter. (Presumably we prefer placing .new at the top in IDEs)

The issue isn't typing, the issue is this standard operation does not have a concise natural representation. This occupies unnecessary space, which is not significant in isolation but can be across a single screen of code. The benefits of concision are emergent and real

To cut it down to .() doesn't make sense, because we'd actually want is _()

It's easy for .() to make or not make sense depending on how one puts it. The fact is that it can easily make sense if one doesn't reject it. . prefix can be thought of as a short-hand indicator. It is then followed by the name of the member. If the member has no explicit name, then there is no explicit name. Hence .() for something like MyClass()


I wouldn't be surprised if some of the younger Dart devs didn't even know that you could do new Class(). So by that standard, new() isn't any more familiar than .().

I agree, and actually I am a relatively new Dart developer who came without knowing the new keyword or constructor name ever existed, so this applies to me

RohitSaily avatar Jun 24 '25 18:06 RohitSaily

Who said anything about familiarity? It's about readability.

If something is familiar it is more readable ie text that uses words and formatting which you are familiar with is more readable because you do not need to expend any mental effort, whether subconsciously or not, to adopt new terminology, styling, concepts, etc

.new() is already part of the language. Anyone who sees it will either understand it from tear offs (which is automatically preferred with a specific lint) or will see it in autocomplete and learn what it means from code docs and go-to-definition. That or they understand it from prior languages or basic dart language docs reading.

I agree with this, it is much more conventional, recognizable, and consistent; however, that should not preclude alternative forms

.() Is just a nothing burger that means the same thing but now you need to learn both what a dot shorthand is, but also what the heck this specific syntax that appears nowhere else even means. And it's easy to forget.

Dot-shorthands are being developed as a language feature, and since it is being adopted and if it becomes official, this concern does not apply because this would be consistent with it

This isn't about introducing some new functionality, this is about syntax/style. However that doesn't make it a "nothing burger", there are trade-offs of different syntaxes/styles

RohitSaily avatar Jun 24 '25 18:06 RohitSaily

To me it sounds like that the problem is with . instead of just having a shorter .new syntax.

The syntax could very well be _():

function(param: _())

This would a throwback to the switch case.
Although by that standard, I'd then use _ as prefix everywhere: _.named(), etc...

rrousselGit avatar Jun 24 '25 18:06 rrousselGit

A period is a rather tiny character. Without it, we have a record instead, which can cause significant confusion, and makes grok-ing harder, because now you have to hunt for that dot.

Again, I agree this can be the case for a significant number of readers. But this can be said for dot-shorthands in general. eg you may have copy as a static function but also use copy as a local name of an actual copy, the only differences in the usages can be copy and .copy. I also disagree with the idea that period is hard to visually identify, if that is the case the problem is the font size for the user and not the fact that a period is used. If it were so hard to visually identify it would not be such a frequently used symbol at all. In addition, this is with monospaced fonts, making them especially easy to identify

I get what you're saying here, but the thing is, we have characters after the dot. this may not seem significant, but it really can be, because now its a member access. its still true that the dot is small, but combined with the static member, your brain sort of glues them together into a member access on... on... oh, its a dot shorthand.

with .() its not a member access, its a unique syntax that doesnt fit any definition. being able to translate a shorthand into the original is important for internal mental constructs. its not a member access, so what is it? a nameless constructor call? no, the dot means its a member access, so that doesnt fit. we'd need to let Foo.() work for that to be consistent.

plus, like i said, people will learn about .new through tearoffs elsewhere, so it becomes downright natural to use .new here too.

It may not save much in text for short type names, but even on its own it does save us from needing to know the name of the type at all, lessening mental overhead

while .() increases mental overhead, because you're using a unique syntax not in use anywhere else, and not consistent with any other dart coding rules. it may be relatively minor, and we can surely get used to it, but... if we add one unique syntax now (thereby locking .() as syntax, making it harder to use elsewhere) then how many might we add later? that stuff snowballs.

We also shouldn't create options where it's not necessary - dart is often opinionated (such as the formatter) so it makes sense to keep it simple.

There exists a standard formatting, but it also permissible to deviate from that and configure your own formatting. In fact, built-in linter rules (or lack of using them) encourage custom formatting

Yes, perhaps that was a bad example - this isn't formatting, its code.

Your talk of advanced users doesn't make sense either, because they'd just use .new() which is plenty short, and easily auto-completed by just typing . + down + enter. (Presumably we prefer placing .new at the top in IDEs)

The issue isn't typing, the issue is this standard operation does not have a concise natural representation. This occupies unnecessary space, which is not significant in isolation but can be across a single screen of code. The benefits of concision are emergent and real

If we need that, we should use _() instead. it makes actual semantic and contextual sense, especially with the mental model that .x === _.x === Foo.x which i mentioned v

To cut it down to .() doesn't make sense, because we'd actually want is _()

It's easy for .() to make or not make sense depending on how one puts it. The fact is that it can easily make sense if one doesn't reject it. . prefix can be thought of as a short-hand indicator. It is then followed by the name of the member. If the member has no explicit name, then there is no explicit name. Hence .() for something like MyClass()

I'm not saying we cant get used to it, but i'm saying it doesnt make semantic sense - it doesnt fit any existing patterns of code, and in face breaks them.

Its important that we keep that consistency in mind, because new programmers are going to be very confused why we can do .() but not Foo.()

the "call" syntax requires you to explicitly do .call() in multiple places - you cant do .() there either, and we already have .new() so we just end up crowding the syntax. too many ways to do the same thing - at least Foo() and Foo.new() is consistent with foo() and foo.call()

allowing a no-name call is super risky for this specific syntax - if we do, we'll need to do the same for normal usages of the access - because as mentioned, its a short-hand for static access. why would we add a special syntax on top of it? Right now it just omits the type name. thats it - its actually a rather simple feature, semantically speaking. this adds something that shouldnt exist in a vaccum.

If we add it here, we should add it everywhere. with that in mind, do you still want the feature? is it actually valuable enough for that?

I wouldn't be surprised if some of the younger Dart devs didn't even know that you could do new Class(). So by that standard, new() isn't any more familiar than .().

I agree, and actually I am a relatively new Dart developer who came without knowing the new keyword or constructor name ever existed, so this applies to me

Thats just because the dart tools dont show .new as a static member when you type Foo. For dot shorthands, i would expect that to change, and suddenly theres a real reason to use it, and now you also become aware you can use it in tearoffs elsewhere - if you didnt know already. after all, its already saving us characters. see these:

final myProvider = NotifierProvider.autoDispose(() => MySpecialNotifier());
final myProvider = NotifierProvider.autoDispose(MySpecialNotifier.new);

which reads better? hell, which is easier to type?

We should expect language features to synergize with each other. If .() gives us no benefit anywhere else, is it actually worth the effort to modify parsing to allow a nameless accessor, and then not use it anywhere else? it basically has to become a synonym for .new() which is not long enough to require shortening. we've already cut the type name - what more do you want?

TekExplorer avatar Jun 24 '25 18:06 TekExplorer

To me it sounds like that the problem is with . instead of just having a shorter .new syntax.

The syntax could very well be _():

function(param: _()) This would a throwback to the switch case. Although by that standard, I'd then use _ as prefix everywhere: _.named(), etc...

Sure! We can do that just fine!

i've said before that _ as the context type makes sense based on both existing code, and basic semantic/textual meaning (_ == the same blank we filled in back in school) expanding that to mean the context type anywhere makes sense to me.

It provides a natural way of doing this with _() which directly translates to Foo() when you "fill in the blank" (not to mention it perfectly matches the existing syntax in switch statements/expressions)

dot shorthands should then be a shorthand to that - its the implicit version. _.foo is the "explicit" version - and even then only because being able to do .thing and .factory() is so pleasant that it becomes downright reasonable to provide that option.

Plus, i'd fully accept linters that suggest explicitly including it, or lints that request excluding it - just as we do for final foo = Foo() vs final Foo foo = Foo()

context type _ becomes a first-class feature. people using the language will be able to understand very simple rules for how it works - no special single-case syntax needs to be memorized, and it can be used elsewhere. (hell, technically speaking final _ foo = Foo() would be valid - if useless. which, actually, gives us exactly the examples for why we can do dot shorthands on top of having _)

Its the same things i've been saying in the original dot shorthands issue for ages.

the way i see it, each feature shouldn't exist in a vaccum. _ as the context type is useful in general (and gives us a target for IDE code docs if we want it) while dot shorthands is a more concise way of typing it (. is easier than shift+-+. after all) while leaving us with the natural _()

as far as i'm concerned, i'm keeping the syntax in line with existing syntax in switch statements/expressions.

TekExplorer avatar Jun 24 '25 18:06 TekExplorer

I believe the originally suggested .() is still better

Pattern

with .() its not a member access, its a unique syntax that doesnt fit any definition. being able to translate a shorthand into the original is important for internal mental constructs. its not a member access, so what is it? a nameless constructor call? no, the dot means its a member access, so that doesnt fit. we'd need to let Foo.() work for that to be consistent.

I think this is conflating syntax and in-depth semantics which is leading to the introduction of syntax which will not be as beneficial as it could be given its conflict with other _ usage. Put simply the dot-shorthands can be modelled as . followed by an identifier and then, if it is a call, the call parameters. This is the simplified pattern

<NameSpace>.<name><parameters> becomes .<name><parameters>

Constructors are normally <NameSpace>.<customName>, the standard new constructor is not named explicitly but does have a name synthesized/implicitly defined for it. So when someone sees the code they just see <NameSpace>. I am not advocating for introducing empty identifiers, but this exists in the explicit sense already. We do not have to arbitrarily extend it, I am just working with the language as it is currently used from this moment onwards

Following this line of reasoning, which is directly derived from the syntax pattern and not semantics in documents or specifications which many users will never actually read, it is reasonable to have .() invoke the constructor. It is a natural fit to the user experience

_

The underscore is widely used in two ways. First is to prefix identifiers to indicate something is library-private. The second is for non-needed identifiers e.g. @override f(int iNeedThis, void _). This isn't too far off in the sense that this is indicating a degree of secrecy, it's just in the first it is implementation secrecy and in the second it is more of a "ignore this, it is meaningless here" secrecy. Either way, each indicate "do not use" to a user

EDIT: There is a third widely used way too, which is the _ wildcard in switch expressions. This still is in line with the other uses in the sense that it still cannot be used and is meant to be ignored in a way since it doesn't express anything in particular, it's just "whatever else"

Introducing _ to be used here is giving it a new way of being used, thus actually increasing the complexity of the syntax and its mapping to semantics, high-level or in-depth. . is already used for shorthands, and it would be much better to stick to it for shorthand accesses/calls/invocations. This is actually consistent with the syntactical design, which is of utmost importance in deciding this, given this is a syntactical feature

RohitSaily avatar Jun 24 '25 19:06 RohitSaily

This is clearly not going to land anyway. I just wanted to open a discussion.

I'll close this :)

rrousselGit avatar Jun 24 '25 19:06 rrousselGit