Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Meta: Small and useful features collection #1077

Open
5 of 15 tasks
lrhn opened this issue Jul 8, 2020 · 35 comments
Open
5 of 15 tasks

Meta: Small and useful features collection #1077

lrhn opened this issue Jul 8, 2020 · 35 comments
Labels
small-feature A small feature which is relatively cheap to implement.

Comments

@lrhn
Copy link
Member

lrhn commented Jul 8, 2020

This meta-issue collects a potpourri of small Dart language enhancements that will thrill users and not cost too much to implement.

We want to choose a subset of these to implement in the next quarter(s).
This issue is for discussion of which of these features offer the most bang-per-buck, and whether there are other low-hanging fruity features to add.

To qualify, the feature should be well-defined enough that it doesn't require significant further design work (so either very trivial or already designed to a large degree), it should be localized enough that it's unlikely to interfere with other language features, and it should not involve the more complicated parts of the Dart semantics (like the type system). If it can be implemented entirely in the front-end (aka, it's syntactic sugar), then that's a great advantage.

The initial features here are mainly syntactical niceties (syntactic sugar) and features which are largely self-contained.
Go vote for your favorite feature by +1'ing its issue.

  • Unnamed library; declaration (#1073)
  • Shorthand for import/export URIs (#649)
  • Parentheses around element expression (#780)
  • Null aware element expression (#323)
  • Unparanthesized function expression parameter (#320)
  • Named arguments everywhere (#1072)
  • Suffix await (#25)
  • &&= and ||= assignment operators (#122)
  • >>> operator (#120)
  • Setter named symbol literals (#301)
  • Number Digit Separators (#2)
  • Binary integer literals (#581)
  • Character code constants (#886)
  • Allow generic function types as type arguments (#496)
  • Generic metadata constructors (#1297)

(See issues sorted by up-votes).

A rough summary of these features is included below.

Unnamed Library Declarations (#1073)

Allow library; to be a library declaration with an empty name.

This is equivalent to no library declaration, but it provides a place to hang library annotations. Currently you have to give a library a name in order to, say, deprecate it, which seems (and really is) unnecessary.

Can also, over time, be used to move library dartdoc away from the import section.

Import/Export Shorthand Syntax (#649)

Allow import shorthands like:

import dart:async; // Same as "dart:async"
import test; // Same as "package:test/test.dart"
import collection:equality // Same as "package:collection/equality.dart"

We'll allow a sequence of some non-space, non-; characters (potentially only ., /, :, letters and digits) to act as a a shorthand for the full URI. We still accept the full URI for cases that contain other characters.

There might be some third-party tools which parse imports using RegExps. If so, those will need to change.

Parenthesized Element Expressions (#780)

Allow

( elementExpression )

as an elementExpression. That allows

var list = [if (t1) (if (t2) e1) else (if (t3) e2)];

which is otherwise not expressible to express without introducing artificial intermediates because the else would bind to the nearest if.

If parsing to an AST, grouping parentheses can be ignored, but the formatter needs to be aware of them.

Null Aware Element Expressions (#323)

Allow ? expression as an element expression, so [?x] is equivalent to [if (x != null) x], but only evaluating x once.

This is a sweet spot of null-awareness, without too much complexity, and it fits well with …?.

Unparenthesized Function Parameter (#320)

Allow (foo) => expr to be written as foo => expr. It only applies to a single untyped argument of an arrow-syntax function expression.

Might not be worth it compared to just having => expr have an implicit parameter of (it).

Named Arguments Everywhere (#1072)

Allow named arguments to occur before positional arguments in an argument list.

Currently you cannot do expectAsync(count: 2, () { … }), but have to put the count argument after the large function body where it's harder to find.

By allowing named arguments before positional arguments, this becomes possible. Evaluation order of arguments is still left-to-right, so it cannot simply be desugared away by moving the argument. The front end can introduce let constructs to force the order of evaluation and then pass the resulting values in the same order as before.

Suffix Await (#25)

Allow e.await in async functions to mean the same as (await e).

Since await as a reserved word in those functions, this will not conflict with any existing code.

It reads much better in long chains of operations because it doesn't need parentheses.

It will also have to work with e?.await, e..await and e?..await, and generally look like a getter. Maybe only allow it on expressions of type FutureOr<T> and Future<T>.

Can be desugared in the front-end.

&&= and ||= Short-circuit Assignments (#122)

Allow x &&= y to mean x ? x = y : false (aka. x && x = y) and x ||= y to mean x ? true : x = y; (aka. x || x = y), where x is only evaluated once as usual.

These can be desugared entirely in the front-end.

Triple Shift (#120)

Implement >>> as a user-declarable operator with the same precedence as >>.

This includes allowing #>>> and const Symbol(">>>") as symbols.

(We can then also implement int.>>> when possible).

Might need minimal backend support as well as front-end changes.

Setter-Named Symbol Literals (#301)

Allow #foo= to be a symbol literal for the setter name foo=. Must work for private symbols too.

Number Digit Separators (#2)

Allow one or more _ characters between digits in number literals. The _s have no effect, they are ignored when figuring out the numeric value of the literal. They work for decimal, hexadecimal and floating point literals, in the latter case also in the exponent section, as long as they are flanked by digits on both sides (where hexadecimal literal digits include the letters a..f, and all other literals don't).

There is no change to int.parse or similar functions. Anyone needing to parse numbers containing _ characters can remove them first using String.replaceAll.

Can happen entirely in the front-end.

Binary Integer Literals

Allow binary integers similar to hexadecimal integers. That would be: 0b1001 for 9.

It's just a new way of writing an integer literal, it can happen entirely in the front-end.
(It's unlikely that we'll want a more general any-radix integer functionality, so this should not conflict with any other feature).

Character Code Constants (#886)

Allow c"x" as a way to write 0x79 (it's an integer literal). The string must contain exactly one code point, so it's a constant expression otherwise equal to "x".runes.first.

Can be implemented entirely in the front-end.

Allow generic function types as type arguments (#496);

Currently Dart does not allow a generic function type, like T Function<T>(T) to be used as a type argument.
This restriction was introduced as a precautionary limit on the type system, but it has been hit by user code, and there is no easy workaround other than using Function and having more unsafe code.
The issue is amplified by they type inference happily inferring a generic type argument, and then the compiler rejects it immediately after.

We hope that simply removing the check will be sufficient, and that no back-end code is affected.

Generic Metadata (#1297)

Allow const constructor invocations in metadata to have type arguments.

@Foo<List<int>>()
int bar = 0;

This is currently not allowed by the grammar. There should be on issues with it.

@lrhn lrhn added the small-feature A small feature which is relatively cheap to implement. label Jul 8, 2020
@rrousselGit
Copy link

rrousselGit commented Jul 8, 2020

May I suggest adding #620 to the list?
It doesn't sound too complex (at least to me, I could be wrong – in which case just ignore it), but is fairly valuable as it gives more flexibility for package authors.
Currently, that restriction prevents exposing a nice API in some situations.

Otherwise, I'd vote for:

These two gives significant readability increase. Named everywhere fits nicely with Flutter. And suffix awaits are a very common use-case

I'd also argue that:

don't seem valuable to me.
Imports are added by the IDE automatically. I have yet to see a situation where I needed the library keyword. And Triple shift/symbols/char code constants are very edge-case.

The ones I haven't mentioned yet looks interesting. Not game-changer, but I can see myself using them.

All in all, the way I'd prioritize it for me, ignoring how long they take, would be in order:


@srawlins
Copy link
Member

srawlins commented Jul 8, 2020

The link for &&= and ||= is wrong.

@lrhn
Copy link
Member Author

lrhn commented Jul 8, 2020

@srawlins Good catch, fixed. (It was #122, not #112)

@mateusfccp
Copy link
Contributor

I agree with @rrousselGit about #620, it would be very valuable.

Regarding the proposed list, I would rank them in this order, from more valuable to less valuable:

  1. Named arguments everywhere (Allow named function arguments anywhere in the argument list #1072) 🥇
  2. Null aware element expression (Null-aware elements #323) 🥈
  3. Suffix await (Prefix await is cumbersome to work with. #25) 🥉
  4. Unparanthesized function expression parameter (Requst: Optional parentheses for one argument arrow function #320)
  5. Character code constants (Add syntax for character code constants. #886)
  6. Shorthand for import/export URIs (Import shorthand syntax #649)
  7. Parentheses around element expression (Allow parenthesized element expressions. #780)
  8. >>> operator (Triple-shift operator #120)
  9. Setter named symbol literals (Allow creating setter-names with symbol literals. #301)
  10. Unnamed library; declaration (Allow library declarations with no name. #1073)
  11. Number Digit Separators (Digit separators in number literals. #2)
  12. &&= and ||= assignment operators (Dart should support &&= and ||= operators. #122)

@comerc
Copy link

comerc commented Jul 8, 2020

Please add short assignment!

MyWidget(child, style) instead of MyWidget(child: child, style: style)

@eernstg
Copy link
Member

eernstg commented Jul 8, 2020

Please add short assignment!

That could be similar to #831.

@lrhn
Copy link
Member Author

lrhn commented Jul 8, 2020

I don't thing #620 is in a state where it's clear what the solution would be. Multiple features are discussed, including omitting/eliding one type parameter from a method taking more than one, or allowing pattern matching to extract nested types from a single parameter type. The former might be a small feature once it's properly designed, depending on the design, the latter definitely won't.
Whatever the solution will be, it will require a significant amount of design work, which disqualifies it from this list. These features are either trivial in design, or already designed to a sufficient degree, and the features are fairly local in scope (not something where we expect large-scale crosscutting concerns with other features). Anything touching type inference is unlikely to satisfy that.

@samandmoore
Copy link

Is #336 a lot of effort to implement? It would make naming classes in large codebases much easier because many classes could be nested within others. Right now we find that we have to name classes with really long names to avoid conflicts and/or confusion.

@lrhn
Copy link
Member Author

lrhn commented Jul 13, 2020

@samandmoore Yes, #336 (nested class declarations) is probably non-trivial.
The compilation pipe-line would require a completely new internal structure to hold the nested classes in order to be able to correctly handle the identifier resolution (or they need to desugar it somehow, so it's only the front-end, but then that too will require some amount of work). And that's even if it's only static nested classes.

@rashedmyt
Copy link
Contributor

Waiting for the >>> operator. Currently using the workaround given here

@fkettelhoit
Copy link

For me the top three would be:

  1. Suffix await (Prefix await is cumbersome to work with. #25)
  2. Named arguments everywhere (Allow named function arguments anywhere in the argument list #1072)
  3. Null aware element expression (Null-aware elements #323)

But even more useful than #323 would be let expressions in collection literals (the idea is mentioned in #323 and perhaps narrowly defined enough that they could be considered as part of this discussion).

Apart from that, shorter function expressions at least for getters (instead of (x) => x.getter, of course #320 would help here a bit) would be much appreciated, but I guess that's better solved using more versatile tear offs and I assume that those are too big of a feature to be included here?

@lrhn
Copy link
Member Author

lrhn commented Jul 14, 2020

Much as I'd love a let expression, I don't think the design is as clear-cut as it looks. Mainly because we might want to go a complete different way instead.
Say instead you could have embedded variable declaration expressions with a scope that is everything dominated by that expression: foo(var list = [], list) would pass the same list twice. That's possibly a better match for Dart than let list = [] in foo(list, list). So, there is some design space here to explore yet before we're ready to add anything to the language.

@fkettelhoit
Copy link

I would strongly favor a let expression in the example you gave. Not only is it likely to be a more familiar construct to users coming from other languages, but I'd argue that it also fits better into the mental model of the current collection literal design (since you just have to remember that the three constructs if, for and let introduce branching, looping and variable assignment using a very similar syntax). In fact, the same syntax that is used for if and for could also be used for let:

final ifExample = [if (a == b) c];
final forExample = [for (final a in b) c];
final letExample = [let (final a = b) c];
final letExampleWithMultipleBindings = [let (final a = b, final c = d) e];

Right now, trying to develop in a declarative style using lots of immutable objects is still quite a pain in Dart, with the exception of collection literals, which really shine in these scenarios. But it is currently frustrating to constantly encounter the road block of having to use the same expression twice and then having to factor out what could have been a declarative collection into a more imperative version.

@mnordine
Copy link
Contributor

mnordine commented Jul 15, 2020

  1. Named arguments everywhere (Allow named function arguments anywhere in the argument list #1072)
  2. Suffix await (Prefix await is cumbersome to work with. #25)
  3. Null aware element expression (Allow parenthesized element expressions. #780)
  4. Unparanthesized function expression parameter (Requst: Optional parentheses for one argument arrow function #320)
  5. Shorthand for import/export URIs (Import shorthand syntax #649)
  6. Number Digit Separators (Digit separators in number literals. #2)

@AKushWarrior
Copy link

I think the shorthand for import/export is a home run. It makes code a lot simpler to understand for those coming from Python/other modern languages; it's also just simpler to type and read.

@rakudrama
Copy link
Member

I would ask to consider including Binary Integer Literals (#581) with Number Digit Separators (#2).

@comerc
Copy link

comerc commented Aug 15, 2020

How about optional semicolon?

@AKushWarrior
Copy link

How about optional semicolon?

I doubt this is a "small" feature.

@lrhn
Copy link
Member Author

lrhn commented Aug 17, 2020

Added #581 to the list, #2 was already there.

@mraleph
Copy link
Member

mraleph commented Sep 8, 2020

@lrhn what about context based enum member resolution (similar to Swift), where instead of E.m one can just write .m if context type is already known to be E. I think this is extremely simple feature to implement - yet it has a very delightful effect, code becomes less repetetive and easier to read (in certain cases).

@lrhn
Copy link
Member Author

lrhn commented Sep 8, 2020

@mraleph While I very much like the idea of enum value shorthand (#357), I don't think the idea is well-defined or mature enough to make this list yet. It's not clear how it should handle == or the {e1?.foo:e2} grammar ambiguity, and we probably also need to check the design around switch statements (it might be sufficient to say that the case expressions get the static type of the switch expression as context type). I'm not convinced that it's trivial, so we need to do the design. Needing to do non-trivial design is a disqualifier for this list.
If we do the design, we can come back and decide that it really is trivial to implement, and then I'd love to add it to the list.

(I notice that I already prematurely gave it the label, so I've removed that again).

@munificent
Copy link
Member

@lrhn what about context based enum member resolution (similar to Swift), where instead of E.m one can just write .m if context type is already known to be E. I think this is extremely simple feature to implement - yet it has a very delightful effect, code becomes less repetetive and easier to read (in certain cases).

Replied here: #357 (comment)

@escamoteur
Copy link

For me definitely Named arguments everywhere (#1072) then we might be able someday to get rid of the named child children parameter and put them as positional parameter at the last position.

I don't know if that is possible without too much work. I would like to see a Result<T>, or MayBe type that are unions with an Error type. I know we don't support unions yet but Dart already has a special Union FutureOr<T> type built in.
IMHO with the introduction of non nullability We need a sound replacement to returning null as a flag to signal errors.

@TimWhiting
Copy link

What about Named Arguments Shorthand #1123? Specifically this proposed syntax: #1123 (comment). It seems like it should be trivial to implement (only adding a bit of syntax and a front end transformation).

@pysta3515
Copy link

Please be stupid, Please be simple.

@skytect
Copy link

skytect commented Dec 18, 2020

Might I recommend #112? It seems simple enough to implement.

@lrhn
Copy link
Member Author

lrhn commented Jan 5, 2021

I don't think #112 is trivial enough, mainly because of the typing.

If you write try { ... } on int, String catch (e) { ... } it's unclear what the static type of e should be.
The most obvious answer is the "least upper bound" of int and String, but the value we choose for LUB is occasionally surprising in a language with multiple and potential diamond-shaped super-interfaces.
Still, it might be able to figure out that SqlFooException and SqlBarException are both SqlExceptions.

Another option is to just say that if you have more than one type in an on clause, the static type of e is Object.
Simple, consistent, predictable, ... not particularly useful.

So, until we can get a decision on that design issue, #112 won't be ready for this list.

@srawlins
Copy link
Member

Is the checkbox list up top rather final? Will these all be guarded by one language experiment flag?

@lrhn
Copy link
Member Author

lrhn commented Jan 29, 2021

Definitely not one experiment flag. We plan to interleave these with larger features when we can find the resources. The criteria for which goes first depends on which resources we have, and if there is an urgent need for something. I hope we'll get them all eventually. :)

@ykmnkmi
Copy link

ykmnkmi commented Feb 11, 2021

Any plans about constant RegExp literals?

scanner.scan(/(\s|\/|>)/);

@lrhn
Copy link
Member Author

lrhn commented Feb 11, 2021

No plans for constant RegExp literals, no. It's actually something that has come up recently in discussion about generalizing constant evaluation, perhaps if it allows constant constructors to validate their inputs. We don't plant to add special syntax for RegExp literals, though. Since we have raw strings, we don't have the same need for a special context for RegExp source as JavaScript, and it's actually remarkably complicated to parse regular expressions with / delimiters.

If you really need a constant Pattern, you can create your own which compiles the RegExp when needed:

class ConstRegExp implements Pattern /* or RegExp if you're ambitious */ {
  static final _cache = Expando<RegExp>();
  final String _source;
  final bool _multiline;
  final bool _caseSensitive;
  
  const ConstRegExp(String source, {bool multiline = false, bool caseSensitive = true, /* etc */})
      : _source = source, _multiline = multiline, _caseSensitive = caseSensitive;

  RegExp get _regExp => _cache[this] ??= RegExp(_source, multiline: _multiline, caseSensitive: _caseSensitive);

  Iterable<RegExpMatch> allMatches(String input, [int start = 0]) => _regExp.allMatches(input, start);
  RegExpMatch? matchAsPrefix(String input, [int start = 0]) => _regExp.matchAsPrefix(input, start);
}

Then you can do:

String foo([Pattern pattern = const ConstRegExp(r"a*b*")]) {
  ..... someString.firstMatch(pattern) ...
}

and get the effect of a RegExp in a constant position.

@gosoccerboy5
Copy link

No plans for constant RegExp literals, no. It's actually something that has come up recently in discussion about generalizing constant evaluation, perhaps if it allows constant constructors to validate their inputs. We don't plant to add special syntax for RegExp literals, though. Since we have raw strings, we don't have the same need for a special context for RegExp source as JavaScript, and it's actually remarkably complicated to parse regular expressions with / delimiters.

What about something like the r"hello" raw string syntax Dart has?
Maybe something like re"\b\w+\b" would return a RegExp made out of the (raw) string provided?

@lrhn
Copy link
Member Author

lrhn commented Sep 16, 2021

It's definitely possible to do re"....." as a literal. It'll likely also need a way to add flags, though.

It's just that the only use for it is to make constant regexps, and ... constant regexps have not been a big need. In practice, a final _re = RegExp(r"....", caseSensitive: false); is usually enough.

Allowing you to inline a regexp, like the scanner.scan(/..../) example above, without creating a new value each time feels like a job for a static operator, scanner.scan(static RegExp(r"....")), which you can implement yourself as:

static RegExp? _re;
...
someFunction() {  
   ...
   scanner.scan(_re ??= RegExp(r"..."));
   ...
 }

(But then you might as well just use the final _re = RegExp(...); instead, which is what I personally do).

@gosoccerboy5
Copy link

It's definitely possible to do re"....." as a literal. It'll likely also need a way to add flags, though.

Maybe something like re"...."gm? I suppose that could look ugly and unreadable..

It's just that the only use for it is to make constant regexps, and ... constant regexps have not been a big need. In practice, a final _re = RegExp(r"....", caseSensitive: false); is usually enough.

My only problem with that is the syntax feels a *tad* verbose to me, having used other languages where RegExps are delimited by slashes. It doesn't make that much of a difference in the end, though, I guess.

@lrhn
Copy link
Member Author

lrhn commented Sep 16, 2021

If verbosity is the problem, then I give you:

extension RegExpShorthand on String {
  RegExp get re => RegExp(this);
  RegExp get i => RegExp(this, caseSensitive: false);
  RegExp get m => RegExp(this, multiLine: true);
  RegExp get u  => RegExp(this, unicode: true);
  RegExp get d  => RegExp(this, dotAll: true);
  RegExp get im => RegExp(this, caseSensitive: false, multiLine: true);
  RegExp get iu  => RegExp(this, caseSensitive: false, unicode: true);
  RegExp get id => RegExp(this, caseSensitive: false, dotAll: true);
  RegExp get mu => RegExp(this, multiLine: true, unicode: true);
  RegExp get md => RegExp(this, multiLine: true, dotAll: true);
  RegExp get ud => RegExp(this, unicode: true, dotAll: true);
  RegExp get imu => RegExp(this, caseSensitive: false, multiLine: true, unicode: true);
  RegExp get imd  => RegExp(this, caseSensitive: false, unicode: true, dotAll: true);
  RegExp get iud => RegExp(this, caseSensitive: false, unicode: true, dotAll: true);
  RegExp get mud => RegExp(this, multiLine: true, unicode: true, dotAll: true);
  RegExp get imud => RegExp(this, caseSensitive: false, multiLine: true, unicode: true, dotAll: true);
}

void main() {
  print("a+b+a+".i.hasMatch("ABBA"));
}

Then you can make a RegExp simply as r"a+b+.*b+a+".id and have it be case-insensitive and dot-all.

Or make a .re(String flags) function if you prefer r"a+b+a".re("i"). The sky is the limit. (Well, and being completely unreadable, but this is regexps after all).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
small-feature A small feature which is relatively cheap to implement.
Projects
None yet
Development

No branches or pull requests