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

Import shorthand syntax #649

Open
lrhn opened this issue Oct 29, 2019 · 79 comments
Open

Import shorthand syntax #649

lrhn opened this issue Oct 29, 2019 · 79 comments
Assignees
Labels
brevity A feature whose purpose is to enable concise syntax, typically expressible already in a longer form feature Proposed language feature that solves one or more problems import-shorthand small-feature A small feature which is relatively cheap to implement. unquoted-uris The unquoted URI feature

Comments

@lrhn
Copy link
Member

lrhn commented Oct 29, 2019

Most recent version:
https://github.com/dart-lang/language/blob/main/accepted/future-releases/unquoted-imports/feature-specification.md

Original design below (which also has some good parts, a final result might be something in-between).


This is a proposal for a shorter import syntax for Dart. It is defined as a shorthand syntax which expands to, and coexists with, the existing import syntax. It avoids unnecessary repetitions and uses short syntax for the most common imports.

Motivation

Dart package imports are fairly verbose because they are based on URIs with no shorthands. A fairly typical import would be:

import "package:built_value/built_value.dart";

The repetition alone is grating, and Dart imports can typically be split into three groups:

  • Platform libraries, import "dart:async";.
  • Third-party packages, import "package:built_value/built_value.dart";.
  • Same package relative import, import "src/helper.dart";.

The package imports are the ones with most overhead. For the rest, the surrounding quotes and trailing .dart are still so ubiquitous that they might as well be assumed.

Syntax

The new syntax uses no quotes. Each shorthand library reference is provided as a URI-like character sequence containing no whitespace, and consisting only of identifiers/reserved words separated or prefixed by colons (:), dots (.) and slashes (/).

The allowed formats are:

  • A single shorthand Dart package name.
  • A shorthand Dart package name followed by a colon, :, and a relative shorthand path.
  • A /, ./ or ../ followed by a relative shorthand path.

A shorthand Dart package name is a dotted name: A non-empty . separated sequence of Dart identifiers or reserved words. Such a sequence can have just a single element and no separator.

A relative shorthand path is a non-empty / separated sequence of dotted names.

The grammar would be:

# Any sequence of letters, digits, `_` and `$`.
<SHORTHAND_IDENTIFIER> ::= 
    <INTEGER_LITERAL> | <INTEGER_LITERAL>? (<IDENTIFIER> | <RESERVED_WORD>)

<DOTTED_IDENTIFIER> ::=
   <SHORTHAND_IDENTIFIER> | <DOTTED_IDENTIFIER> '.' <SHORTHAND_IDENTIFIER>

<SHORTHAND_PATH> ::=
   <DOTTED_IDENTIFIER> | <SHORTHAND_PATH> '/' <DOTTED_IDENTIFIER>
   
<SHORTHAND_URI> ::=  
    <DOTTED_IDENTIFIER> (':' <SHORTHAND_PATH>)?
  | './' <SHORTHAND_PATH>
  | '../' <SHORTHAND_PATH>
  | '/' <SHORTHAND_PATH>
   
<uri> ::= ...
        | <SHORTHAND_URI>

Since a shorthand URI can only occur where a URI is expected, and a URI is currently always a string, there is no ambiguity in parsing. Tokenization is doable, but will probably initially allow whitespace between tokens because it doesn't yet know that it's a shorthand sequence. When it recognizes that a URI is expected and a non-string follows, it must combine the following tokens only as long as there is no space between them.

We can allow spaces between identifiers/keywords and :, . and /, but it will be harder to read and it makes the grammar less extensible.

The shorthand syntax can also be used for export and part declarations. It does not work for part of declarations because part of foo.bar.baz; is already valid syntax. We could allow only relative (./ or ../) shorthands for part of declarations, or we may want to disallow this existing syntax so that you can use the full shorthand syntax with no exceptions.
(Please do disallow the old part-of format where you use the parent library name).

(Not sure this works as written. Something like x.2.4e2 can be parsed as containing a double literal. The grammar above doesn't allow that. A less distinguishing approach could be:

  • Require a shorthand to start with [a-zA-Z_$0-9./].
  • Include every character up to the first following ;, whitespace, quote or comment. (Tokenization will be very confusing if the shorthand URI can contain // or /*, or a string quite.)
  • Check later whether its valid according to the grammar above.

That seems more viable than trying to guess which tokens can be accepted, just accept any that can be included whole into the combined lexeme.)

Semantics

An import of a single-identifier package name, name, is equivalent to an import of "package:name/name.dart". This is the most common form of package imports, and it gets the shortest syntax.

An import of a dot-separated package name, some.prefix.last, is equivalent to an import of "package:some.prefix.last/last.dart". The single-identifier case is just the special case where there is no prefix.

An import of a package-colon-path sequence, name:path is equivalent to an import of "package:name/path.dart". (Notice the added .dart). This is used for packages which expose more than one library.

An import of a relative URI path, path, one starting with /, ./ or ../, is equivalent to an import of "path.dart".

The package name dart is special cased so that an import of dart:async will import "dart:async", and an import of just dart is not allowed because there is no dart:dart library. This allows us to treat dart: as a platform supplied package with libraries core.dart, async.dart, etc., which may actually be an improvement over the current special-casing that we do. It does mean that dart is not available as a package name for user packages.

Examples:

  • import built_value; means import "package:built_value/built_value.dart";
  • import built_value:serializer; means import "package:built_value/serializer.dart";.
  • import dart:async; means import "dart:async";.
  • import ./src/helper; means import "src/helper.dart";.
  • import /src/helper; means import "package:current_package/src/helper.dart";.

The leading ./ for relative files in the same directory, is needed because otherwise we cannot distinguish whether import foo; means import the foo package or the local foo file.

  • import hide hide hide; is valid and means import "package:hide/hide.dart" hide hide;.
  • import pkg1 if (dart.libraries.io) pkg2; works too, each URI is expanded individually.

Consequences

Programmers can write less code. There will be some paths which cannot be written in the shorthand syntax, perhaps because they contain non-identifier characters or path segments starting with a digit. Those will still have to be written the old way, inside delimited strings.

The parser needs to be a little clever. If it tokenizes identifiers, reserved words, dots, colons and slashes first, then it has to combine them back into a single shorthand URI and check for separating whitespace. The reason this proposal does not allow even more complicated shorthand URIs is that it would make parsing even more problematic. The chosen design attempts a trade-off between allowing most existing package URIs to be written with the new syntax and allow the syntax to be parsed without too much overhead.

@lrhn lrhn added the feature Proposed language feature that solves one or more problems label Oct 29, 2019
@eernstg
Copy link
Member

eernstg commented Oct 29, 2019

We can always make the choice to enforce extra rules for whitespace, but I don't even think that's particularly important. The following approach is grammar based (so whitespace is allowed everywhere), and it parses the examples without issues, as well as all the usual test files (so the grammar isn't broken): https://dart-review.googlesource.com/123407.

@mnordine
Copy link
Contributor

mnordine commented Oct 29, 2019

This is one of the things that drove me crazy from the start. Would love to see this change.

I'd rather see dart.async instead of dart:async though.

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

Overall, yes, I like this and think it can work.

This is mostly a matter of taste, but I find it hard to like the leading ./ for relative paths and using / as a separator in an unquoted "path".

It's a shame to give the elegant foo.bar.baz syntax over to only be used for the internal dotted package names. It would nice if that could mean package:foo/bar/baz.dart' since that would benefit external users too.

To get a better picture, I scraped a corpus and tried to gather (or in the case of internal code, fake) a representative set of imports. Here is the current syntax:

import 'dart:isolate';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart';
import 'package:flutter/material.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:widget.tla.server/server.dart';
import 'package:widget.tla.proto/client/component.dart';
import 'test_utils.dart';
import '../util/dart_type_utilities.dart';
import '../../../room/member/membership.dart';
import 'src/assets/source_stylesheet.dart';

I think that covers all the different forms. With this proposal, those become:

import dart:isolate;
import flutter_test;
import path;
import flutter:material;
import analyzer:dart/ast/visitor;
import widget.tla.server;
import widget.tla.proto:client/component;
import ./test_utils;
import ../util/dart_type_utilities;
import ../../../room/member/membership;
import ./src/assets/source_stylesheet;

To me, the unequivocal wins are flutter_test and path. I think dart:isolate and flutter:material look pretty good. The rest are OK, though the slashes look a little strange to me.

Here is an alternate idea:

import dart:isolate;
import flutter_test;
import path;
import flutter:material;
import analyzer:dart.ast.visitor;
import widget.tla.server;
import widget.tla.proto:client.component;
import 'test_utils';
import '../util/dart_type_utilities';
import '../../../room/member/membership';
import 'src/assets/source_stylesheet';

The rules here are:

  • Package imports are unquoted.
  • Use a : to separate package name from path.
  • Use . as the path separator for packages.
  • Non-package imports stay quoted and use / for separators. They do not need .dart.

Using the . as a package path separator makes it more like a "logical" path and makes it look more natural to me when not quoted. Quoting file paths makes 'test_utils' unambiguous without needing a leading ./ (and is just as long, though ../ imports get two characters longer). I don't mind quoting file paths—I like that it makes the / feel a little more natural to me and doesn't require clever parsing tricks. It reminds me of the distinction between #include <foo> and #include "foo" in C.

Using quoted file imports does mean implicitly adding .dart to any existing file import that lacks it, which could potentially be a breaking change. I searched the 1,000 most recent pub packages and the only non-"dart:" I could find that didn't end up "package:" were:

Dart:async
src/notadotdartfile
dart-ext:ejdb2dart

So I think we're fine there.

Again, I think the style proposed here is workable too. Either of these options would be a net improvement, I believe.

@mnordine
Copy link
Contributor

Both are an improvement, but I'd prefer the latter. The former is weird with unquoted paths, particularly files from the same directory with the leading ./

@lrhn
Copy link
Member Author

lrhn commented Jul 15, 2020

I must admit that using . as path separator is not something that feels natural to me.
It also has the issue that . might be a part of a directory or file name. It already happens for, say, generated protobuf files, something.pb.dart. Using import foo:src.generated.something.pb; becomes ambiguous.
A / can't be part of a directory name, not even on Windows.
A dotted identifier also looks like a library name, which is confusing. If you write foo:pkg.foo.lib, I'd be inclined to read it as trying to import the library named pkg.foo.lib.

Using dots does make it look like a namespace. It's just that it isn't, It's not an internal Dart scope thing, but rather navigating in an external hierarchical path structure where the parts are not necessarily Dart identifiers, not even today, and making it look like it's Dart names worries me.

As for:

import 'src/assets/source_stylesheet';

without the trailing .dart, that's a breaking change because Dart files don't need to end in .dart. They typically do, but that import could already be valid, and now there'd be no way to actually import that file.

I think quoted strings should keep their current meaning, an unquoted imports (or differently quoted strings) are shorthands for an actual URI reference.

@lrhn
Copy link
Member Author

lrhn commented Jul 15, 2020

We might want to special-case dart-ext: as well, or maybe we won't, and you have to write it as a URI. It's rare enough that it probably doesn't matte.

@eernstg
Copy link
Member

eernstg commented Jul 15, 2020

We'd have to preserve the ability to write imports with quotes, because file names can contain special characters (even a plain space would make parsing hard, e.g., hide hide hide could be a file name). We might then conclude that we should avoid "magic" rules (including adding .dart at the end) in these old-style imports. which would make old-style imports a safe option, usable for code generators and with tricky file names.

Considering the "nice" paths that don't contain spaces and other obstacles, I don't see a problem with the slashes, and I actually tend to think it's good for readability that the syntax is similar to the URIs and paths that we see in many other contexts. Of course, focusing only on these "nice" cases and with no existing code to break, it's obviously an attractive idea to leave out .dart at the end.

This leaves the initial ./ in paths like ./test_utils or ./subdir/test_utils as one of the main controversies.

How about reusing the colon, e.g., import :test_utils;?

We would use : to indicate that the following [a-zA-Z0-9_/]+ is a relative path. It's one char shorter! 😄

You might want to think that it's justified by "the current package can be denoted by the empty string", but that is actually not true (e.g., for a relative import of a library in myPackage/test/subdir from myPackage/test, using import :subdir/myLibrary; has a different meaning than import myPackage:subdir/myLibrary;). Still, that shouldn't be too hard to get used to.

@lrhn
Copy link
Member Author

lrhn commented Jul 15, 2020

I'd actually expect :foo to mean <current package>:foo, an absolute path into the current package. It seems arbitrary to make it relative to the current location, and different from the other uses of : to delimit package from path.
Then

import :src/helper.dart

would be another way to refer to local files, absolutely rather than relatively, but still relative on the package name.
It's equivalent to the current

import "/src/helper.dart";

We could consider adding that as a feature too.

@munificent
Copy link
Member

I'd actually expect :foo to mean <current package>:foo, an absolute path into the current package.

Me too.

Then

import :src/helper.dart

would be another way to refer to local files, absolutely rather than relatively, but still relative on the package name.

I considered a proposal where that was the only new sugar for "relative" imports. Basically tell users to make everything a package import, even for libraries in their own code. That obviously doesn't work for libraries outside lib. But even ignoring that, I found many many imports in a corpus that would become egregiously long if you had to use a full path from the root of the package.

It's equivalent to the current

import "/src/helper.dart";

We could consider adding that as a feature too.

That... that works? :-O

@lrhn
Copy link
Member Author

lrhn commented Jul 16, 2020

It's equivalent to the current

import "/src/helper.dart";
We could consider adding that as a feature too.

That... that works? :-O

Ack, no. I thought I had changed package: URI resolution to keep the name absolute. Apparently I didn't finish that yet.
https://dart-review.googlesource.com/c/sdk/+/117542

(Update: It works!)

@listepo
Copy link

listepo commented Jul 24, 2020

@lrhn @munificent @eernstg What do you think of index.dart for folder widgets? like index.js

widgets/button.dart
widgets/index.dart => export 'widgets/button.dart';
some.dart => import 'widgets';

@lrhn
Copy link
Member Author

lrhn commented Jul 24, 2020

The idea, from a very cursory glance, seems to be that a directory can contain a "default" file that is imported if you import the directory. In Dart, it would mean that

import "package:foo/bar/";

would automatically import "package:foo/bar/index.dart".

We have traditionally used the package name as the default file in dart, so "package:foo/foo.dart" is the default name for "package:foo".
We could extend that to any directory, so if you refer to foo/bar/baz and that turns out to be a directory, we "complete" it to "foo/bar/baz/baz.dart".

Not sure whether that's better or worse than what is proposed here. You'll have to write less for sub-directories, but it requires the compiler to be able to recognize directories, something it can't if it fetches source from an HTTP URI.

@listepo
Copy link

listepo commented Jul 24, 2020

We could extend that to any directory, so if you refer to foo/bar/baz and that turns out to be a directory, we "complete" it to "foo/bar/baz/baz.dart".

I think this is also a good option

@munificent
Copy link
Member

I'm personally not a fan of adding syntax that relies on a convention for organizing libraries that doesn't already exist. I think this convention is already pretty well-established:

widgets/button.dart
widgets.dart => export 'widgets/button.dart';
some.dart => import 'widgets.dart';

I'd rather have a syntax assume that convention and then users don't have to reorganize their code to get the greatest benefit from the new syntax.

@listepo
Copy link

listepo commented Jul 24, 2020

widgets/button.dart
widgets.dart => export 'widgets/button.dart';
some.dart => import 'widgets.dart';

@munificent that's how I now use, but it's not so convenient. My personal subjective opinion.

@r3flow
Copy link

r3flow commented Oct 12, 2020

Is this mean that the following example will be valid?

import /module/foo/xy.dart; means import 'package:my_app/module/foo/xy.dart';

An absolute import option from the root of the project/package would greatly increase the reusability of the code.

@lrhn
Copy link
Member Author

lrhn commented Oct 12, 2020

I think that should work. The current proposal doesn't include a package-root-relative path, but I see no reason it can't.
(So I added it).

@ykmnkmi
Copy link

ykmnkmi commented Mar 11, 2021

also import @async or import :async for import 'dart:async' and import _/some.dart for current package.

@dart-lang dart-lang deleted a comment from listepo Apr 20, 2021
@cedvdb
Copy link

cedvdb commented Apr 21, 2021

One thing useful in typescript is the ability to create aliases for imports.

so you can import like this

import '~core';
import '~models';

@mit-mit mit-mit changed the title Import shorthand syntax. Import shorthand syntax Sep 27, 2021
@Hixie
Copy link

Hixie commented Dec 21, 2023

Are there objections to the proposal wherein imports are either a string (works like today), or a slash separated series of identifiers, where:

  • a single identifier import foo turns into a package-import of that package with the same library name (import 'package:foo/foo.dart').
  • a slash indicates a path into the package given as the first identifier (as in import foo/bar/baz becomes import 'package:foo/bar/baz.dart').
  • the dart package is treated as shorthand for dart: pacakges (as in, import dart/async is import 'dart:async').

...?

(Identifiers in this proposal are really dotted-identifiers, as in import foo.bar is import 'package:foo.bar/foo.bar.dart').

As far as I can tell this proposal handles all the common patterns and solves the "imports are a pain to write and ugly to read" thing that really is what for me motivates my interest in this issue.

@lrhn
Copy link
Member Author

lrhn commented Dec 21, 2023

That's effectively "remove leading package: and trailing .dart, and if that leaves you with name/name, shorten it to just name". (And treat dart/ specially)

That works. It doesn't provide anything for relative paths, but those can use quotes. I'm OK with that. Probably for the best.

It doesn't address package names with .s in them, which are used by some non-Pub based build systems. Or it does, they just don't get the final shorthand, and must write foo.bar.name/name.
Also OK, this is a shorthand for the common case, doesn't have to cover every special-case.

It can work.

I'd prefer to use : to separate the package from the path, because using slashes for everything makes it look to much like a relative path, which can be confusing. People may think a relative "../other_package" could work.
But this has simplicity going for it.

Should probably allow the path segments to not just be identifiers, but any sequences of ASCII letters, digits, underscore, dash and dot, as long as they're not all dots.
That differs from identifiers only in allowing . and - too, and a leading digit.

@Wdestroier
Copy link

I would like to suggest a non-breaking change which can happen without the complete proposal in place.

Allowing import blocks

import {
  'dart:async',
  'package:path/path.dart',
  'package:flutter/material.dart'
}

Instead of writing import multiple times

import 'dart:async';
import 'package:path/path.dart';
import 'package:flutter/material.dart';

Mainly to improve readability, but requires less keystrokes the more imports we have.

@lrhn
Copy link
Member Author

lrhn commented Dec 21, 2023

import {
  'dart:async',
  'package:path/path.dart',
  'package:flutter/material.dart'
}

Not completely impossible.
Probably want to use semicolon as terminator/separator, just to make it easier to recognize the end of a show clause.

Unless the test of the import clause goes after the }, so it's easy to import multiple libraries into the same prefix.

Still very little real benefit for adding new syntax.

@iapicca
Copy link

iapicca commented Dec 21, 2023

import {
  'dart:async',
  'package:path/path.dart',
  'package:flutter/material.dart'
}

Still very little real benefit for adding new syntax.

@lrhn
IMHO it does look leaner than

import 'dart:async';
import 'package:path/path.dart';
import 'package:flutter/material.dart';

@Wdestroier
Copy link

Probably want to use semicolon as terminator/separator, kids to make it easier to recognize the end of a show clause.

Thanks, I forgot. Can be an opportunity to remove the semicolon/terminator from imports as part of a larger effort to address issue #69.

@SpencerRiddering
Copy link

SpencerRiddering commented May 14, 2024

Rather than complicating the Dart language, why not implement this as an optional IDE code style or IDE plugin?

Edit: I posted a question, so if you are going to down-vote me then how about explaining?

@lucavenir
Copy link

lucavenir commented May 14, 2024

Downvotes are a simple and quick way to say: "nah".

With the context of your message people probably wanted to say "nah, there's no complication; rather, your optional code style proposal is actually way more complicated, while also not compliant with dart philosophy about formatting (it can't be opinionated)"

@tatumizer
Copy link

tatumizer commented May 14, 2024

This ER is trying to fix something that is not broken. Or at least not broken enough.
Sure, 'package:' and '.dart' are redundant - I get it. It would be possible to allow dropping them (e.g. by replacing package:foo/ prefix everywhere with foo:). IDE could suggest the refactoring, and migrate the old syntax to the new syntax (still with quotes!). Replacing "path/foo/foo.dart" by "path/foo" is straightforward, too.

The idea of dropping quotes, however, goes a bit too far IMO: the beauty of the unquoted paths is questionable - but on top of that, not every import can be migrated to the new syntax. People use Unicode characters in their paths (I've seen such examples, especially in tests), so they will have no option but to preserve the legacy syntax. (Do unquoted paths really look so much better? How many people will feel unhappy if the quotes are preserved?)

The current import system in dart is very simple - much simpler than in most languages. E.g. rust import syntax looks nicer, but it imposes cumbersome restrictions on code organization. This is the price to be paid for a nicer syntax. :-)

EDIT: if the goal is to improve readability, from packageName import 'path/to/file'; is a good candidate.

@mateusfccp
Copy link
Contributor

mateusfccp commented May 15, 2024

The idea of dropping quotes, however, goes a bit too far IMO: the beauty of the unquoted paths is questionable - but on top of that, not every import can be migrated to the new syntax. People use Unicode characters in their paths (I've seen such examples, especially in tests), so they will have no option but to preserve the legacy syntax. (Do unquoted paths really look so much better? How many people will feel unhappy if the quotes are preserved?)

Maybe we should just do it and support Unicode anyway, just like we should for identifiers (#1283, which was, unfortunately, closed).

@SpencerRiddering
Copy link

SpencerRiddering commented May 15, 2024

Of all the challenges I face when writing Dart/Flutter code, "unnecessary repetitions" in my import statements is near the bottom of the list. I almost never read through the imports, and 99.9% of the time I don't even write the code because the IDE adds it automatically.

@pedromassango
Copy link

Of all the challenges I face when writing Dart/Flutter code, "unnecessary repetitions" in my import statements is near the bottom of the list. I almost never read through the imports, and 99.9% of the time I don't even write the code because the IDE adds it automatically.

I agree that we have more important stuff to discuss, why bother about "unnecessary repetitions" that the user never writes by himself?

Pls, let's focus on getting meta-programming out and in a ready-to-use state so we all stop the "unnecessary repetition" of running code-gen tools (build_runner) which would solve the most basic issue we all need a fix for: class equality out of the box

@munificent
Copy link
Member

For those following along, I've written an alternate proposal here based largely on this comment.

@mmcdon20
Copy link

@munificent

I'm curious why

import 'dart:isolate';

becomes

import dart/isolate;

rather than

import dart:isolate;

The / makes it look like import dart/isolate; would be shorthand for

import 'package:dart/isolate.dart';

Just as import flutter/material; is shorthand for

import 'package:flutter/material.dart';

@lrhn
Copy link
Member Author

lrhn commented May 19, 2024

@munificent For post-tokenization parsing, I'm starting to think that the best approach is to include all tokens up to the next whitespace, comment or semicolon, then reject if any of those tokens contain a non-accepted character. I think that means only accepting IDENTIFIER, RESERVED_WORD (I want to include those), NUMBER, HEX_NUMBER, /, . and - (which I also want to include), and then rejecting also if an IDENTIFIER contains a $, a (floating point) NUMBER contains a +, or any pair of /, . or - are not separated by one of the alpha-numeric tokens. That gives a sequence of [a-zA-Z0-9_]+s separated by ., / or -.

The reason for including all the tokens is to have better error recovery. The shorthand starts at the next token after the import, and ends at the first whitespace, ;, // or /*. Then it's an error if that contiguous block of non-whitespace characters contains an invalid character, but we know where to end it, instead of ending early on a character, and then trying to parse the following characters as if they were not included. Take: import foo:bar/bar%20baz;. Someone might think that's allowed. The error message they get is that % is not allowed in a shorthand URI, not that % is unexpected.

@tatumizer
Copy link

tatumizer commented May 19, 2024

Dots in the names are the root cause of all import syntax problems.
From the proposal:

In fact, inside Google's monorepo, dotted package names are idiomatic and universally used. If our import shorthand syntax couldn't hangle package names with dots in them, no one inside Google would be able to use it.

I wonder how dotted packages are used with other languages at google. Maybe there's a bit of extra configuration involved?
(E.g., by using a "path" annotation, or via yaml file, or something).
Also, there's an option to NOT migrate these packages into new format at all.
The proposal cites the problem of user sentiment as a rationale, but the user at large (beyond google) very rarely has to deal with dotted names, which are used by just 0.391% of imports (according to the stats from the same proposal).

I understand, internal users are humans, too, and their sentiment certainly counts. But maybe they can agree to pay a small tax in the form of a bit of extra configuration? That will allow the rest of the users to enjoy some more familiar format with dots or :: as separators?

@lrhn
Copy link
Member Author

lrhn commented May 20, 2024

Dots in the names are the root cause of all import syntax problems.

All?

I think they're only problems if you want to use . as the main separator.
I have never wanted that, probably because dotted identifiers are already library names in Dart, and I was aware of dotted package names, and of directory and file names with dots in (fx foo.g.dart).

That is, every part of the thing we want to join up can already contain dots, so using dots as the separator between them never seemed viable, no more than using _.

And then, there is no problem with .s.

(Even if there were no existing dotted package names, I'd still want to keep the option open, in case Pub's linear namespace gets too crowded, and we want to open up to Java-like domain based package names like com.example.json.)

@tatumizer
Copy link

tatumizer commented May 20, 2024

I think they're only problems if you want to use . as the main separator.

Not only. If you try to use :: (as in rust, ruby) as a separator, and dots are still a problem. E.g. you can't introduce canonical fully qualified names for the imported symbols. In code generation, dart::int could refer to int type directly; without FQN, you have to write it with an artificial prefix like prefix0.int, which is awkward and cryptic, plus the prefix may change in another file. Who knows where fully qualified names may come in handy in the future. With dots, you can't say foo(a.b.c::d::MyType).

(remember, my question was: how dotted names are used at google with other languages? E.g. rust? Rust doesn't allow dots in module names. What gives?)

Let's focus on the aesthetics of the import statement. The current format is nothing to brag about, for sure, but at least the ugly part is quoted. But without quotes, the syntax becomes strange and foreign: it doesn't rhyme with anything else in dart, cannot be reused in any other context, and leads to puzzlers like this

import foo/bar;  // based on munificent's proposal
import 'foo/bar.dart'; // relative import from the current package

These two lines look similar, but their meaning is quite different. Same with

import foo;
import 'foo.dart'

Turns out, the 'package:' prefix was not that redundant; when you eliminate it, the notation becomes more cryptic and even more unfamiliar. Using colons and slashes doesn't make it much better.
And then, dots, dashes, NUMBERs... When the parsing algorithm becomes so complicated, isn't it a signal that something is a bit off with the syntax itself?

(An argument can be made that .dart suffix is not that redundant either. I speculate that it was put there to help with the cases like import 'package:path/path', which indeed looks slightly puzzling without the .dart).

EDIT: here's another attempt to verbalize my grievances.
The dotted syntax suggests some logical hierarchy, but the slashes signify a physical hierarchy, which also attempts to recast itself into a logical hierarchy; the two hierarchies mixed together reveal the lack of a coherent concept.

@lrhn
Copy link
Member Author

lrhn commented May 20, 2024

With dots, you can't say foo(a.b.c::d::MyType).

Sure you can. It's trickier to parse because :: makes the prior tokens be interpreted differently than if they were a stand-alone expression, but it's not impossible. But it does mean that you have to know where to start. A packageName::filePath::identifierName can't omit the package name when everything has the same format (but everything can be a single identifier anyway, so that's not because of the dots.

It's not how I'd define it, I'd probably go with a.b.c/d::MyType, and see if I can parse that. That is, start with a library designator (URI) to identify the library by URI.

@tatumizer
Copy link

But then, I have issues with this statement

That is, every part of the thing we want to join up can already contain dots, so using dots as the separator between them never seemed viable, no more than using _.

But if a.b.c/d::MyType can be made viable by adding a bit of complexity to the parser, I can claim that the same, written in the form a.b.c.d.MyType can be made viable, too - by adding a bit more complexity. The point is that all the packages used in your project are known (they are listed in .yaml file), then you can cache all composite package names and figure out what belongs to what - and this will work even if you have the directory structure a.b/c.d or a.b.c/d - in case of ambiguity, the compiler should complain - but it should complain anyway if it encounters a logical ambiguity.

Example: suppose there are 2 files called foo.dart, one defined in a.b/c/foo.dart, another - in a.b.c/foo.dart. The logical names of these foos are better to be considered identical, unless we want to create even more confusion.
So any ambiguity has to be reported regardless of the syntax.

Caching the catalog of available packages is not unheard of (I suppose many build systems do that, especially with jar files).
Again, it's highly desirable to avoid mixing logical and physical concepts. Many languages go into great lengths trying to come up with a unified concept.

Maybe this is the right way to go? Suppose you want to use a globally unique name com.myCompany.json for your package. Go ahead and create such a package, place its components under /lib, and now you can use import com.myCompany.json.serializer;. On disk, the package will be represented as a directory with a dotted name com.myCompany.json (package root) with all the standard subdirectories, as usual. This is arguably better than java's convention that forces the user to physically create com, com/myCompany, com/myCompany/json etc.

WDYT?

@jakemac53
Copy link
Contributor

jakemac53 commented May 20, 2024

There are many tools today which expect to be able to look at an import and know which part of it is the package name and which part is the path, and do not today need the entire package config to do this (as an example, the lint that you depend on referenced packages).

This is not only useful for tools, but also useful for humans. I am strongly opposed to creating an import syntax where you cannot easily and syntactically, without global knowledge, know which part is the package name and which part is the path.

@tatumizer
Copy link

Maybe I'm playing devil's advocate, but in rust, it's not that easy to find out how the logical structure is mapped to physical structure by just looking at the code (see https://stackoverflow.com/questions/73840738/how-exactly-does-rust-look-up-modules)
That's what tools are for.

Wouldn't it be useful for humans to be able to look at the declaration var foo=Foo(); (in dart code) and immediately figure out where this "Foo" came from? Maybe. But this information is not readily available in the code.
In java, when you import some com.companyName.etc, the implementation comes from some jar file. Can you tell from which one, just by looking at the package name?

@jakemac53
Copy link
Contributor

Isn't the existence of that stack overflow question itself indicative of the problem here?

In dart we have a very concrete answer to that question. You look up the package name in the package config (at .dart_tool/package_config.json by default), and everything after the package name in the URI is relative to the lib root you see there. That seems valuable to me.

Just because you can't do that easily in other languages, doesn't make it bad?

@tatumizer
Copy link

tatumizer commented May 20, 2024

Isn't the existence of that stack overflow question itself indicative of the problem here?

Sure it is! But only if we consider the issue of import in isolation.
An interesting question is why these languages were designed this way. I can only speculate, but here it is:

  • they needed fully qualified names.
  • they wanted a nice import syntax.
  • they wanted to maintain a simple hierarchical structure (a thing like a.b.c/d/e.f is not suggestive of a simple structure - rather, it is reminiscent of a fractal or something)
  • the answer to the question "where does this import come from?" is just one (right) click away, and this right click is the same kind of a click that answers the question "where does this type come from?", so we don't gain much by singling out one question among many.

(Probably there's more - I'm just guessing).

Back to dart's import syntax: I think I made too big a deal out of dotted names. A typical user won't see such names very often; dart can support them without recommending them. But slashes don't play well with FQNs.
The following is good (in generated code): final dart::int x;. This is ugly (and probably not parseable): final dart/int x;.

In java, you never encounter underscores in package names (the style guide recommends com.example.deepspace over com.example.deepSpace or com.example.deep_space). So if in the future someone decides to create a package with a globally unique name, they can use underscores rather than dots: com_example_deepspace_foo. No dots are necessary.

Using :: (rather than /) as a separator is preferable also to make the distinction between absolute imports and relative ones more pronounced (assuming that the latter continue to use legacy syntax).

You may also consider allowing to refer to the current package as :: while converting relative imports to absolute imports.

@lrhn
Copy link
Member Author

lrhn commented May 21, 2024

A fully qualified name that includes the library, will include the equivalent of a URI, because that's what Dart uses to identify a library.
Using a shorthand for the URI requires having a shorthand to begin with.

I'm not opposed to something like URI:: identifier being able to denote any identifier, but I would still require the library to be imported first (I don't want to have to parse every function body to give the library dependencies).
At that point, you can just import with a prefix, in a systematically named way, so that you can write dart__core.int or expect__async_helper.asyncStart.
Not as pretty, but just as unambiguous.

The goal here is to give a shorter, less noisy, syntax for import URIs, not to solve "qualified references in code".
If the same syntax can be used for both, then it's obviously better, but dart:core::identifier could probably be used for that, with some parsing complexity, but no semantics ambiguity.
Or maybe it needs a leading delimiter.
And "dart:core"::int and "package: expect/async_helper.dart":: asyncStart can work to. The reason the shorthand import syntax works is that the start point is known.

@kallentu kallentu added the unquoted-uris The unquoted URI feature label Sep 10, 2024
@eernstg eernstg added the brevity A feature whose purpose is to enable concise syntax, typically expressible already in a longer form label Oct 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
brevity A feature whose purpose is to enable concise syntax, typically expressible already in a longer form feature Proposed language feature that solves one or more problems import-shorthand small-feature A small feature which is relatively cheap to implement. unquoted-uris The unquoted URI feature
Projects
Status: Spec complete
Development

No branches or pull requests