Skip to content

Are import shorthands worth the cost in readability/cognitive load? #1941

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

Open
leafpetersen opened this issue Oct 26, 2021 · 114 comments
Open

Are import shorthands worth the cost in readability/cognitive load? #1941

leafpetersen opened this issue Oct 26, 2021 · 114 comments
Labels
brevity A feature whose purpose is to enable concise syntax, typically expressible already in a longer form import-shorthand unquoted-uris The unquoted URI feature

Comments

@leafpetersen
Copy link
Member

Reading the import shorthand proposal, I am struck at the level of complexity for a user that this is introducing. Consider this text:


Examples:

- `import built_value;` means `import "package:built_value/built_value.dart";`
- `import built_value:serializer;` means `import "package:built_value/serializer.dart";`.
- `import :src/int_serializer;` means `import "package:built_value/src/int_serializer.dart";` when it occurs in the previous `serializer.dart` library, or anywhere else in the same Pub package.
- `import ./src/int_serializer;` means `import "./src/int_serializer.dart"`, aka.`import "src/int_serializer.dart"`, when it occurs inside the previous `serializer.dart` library.
- `import ../serializer;` means `import "../serializer.dart"` when it occurs inside the previous `src/int_serializer.dart` library.

This is an extraordinary amount of new cognitive load for a user. There are now somewhere around 8 ways to write an import of serializer.dart, some of which are only valid in certain locations, and all of which use overlapping syntax to mean different things (":something means the package part was elided, but foo:something means add the package: back in, change the : to a / and add a .dart"). There's numerous ways to write the same thing:foo and foo:foo and package:foo/foo.dart and :./foo and .foo and "./foo.dart" and "foo.dart" all might mean the same thing (I think? I'm a little lost in the weeds.).

Is this really worth the shorter syntax? I really worry that we're making code significantly less consistent and readable in the name of brevity.

cc @lrhn @munificent @eernstg @jakemac53 @natebosch @bwilkerson

@jakemac53
Copy link
Contributor

Note that pub run also already has a meaning for built_value:serializer which is different, it means the serializer executable in the built_value package, which would usually be at bin/serializer.dart.

If we shipped this feature with the : syntax it might be very confusing for users if they do dart run foo:bar, they may expect it to mean package:foo/bar.dart.

@srawlins
Copy link
Member

I also worry about using : as the delimiter between a package name and a top-level library in that package (import build_value:serializer), as it is already a delimiter in the quoted syntax, in, e.g. "package: and "dart: (and I don't think others? It doesn't accept "file://, right? You can't reference Windows driver letters, right?)

I'm sure just about any other delimiter looks weird too though.

  • #? import built_value#serializer? Looks a bit like a URL anchor.
  • @? import built_value@serializer? This is reminiscent of Node's scope (require('@myorg/mypackage')).
  • /? import built_value/serializer? This looks like it could be confused with a relative path, but I think relative paths are not support with the un-quoted style? Either an absolute path or a ./ path is valid.

@srawlins
Copy link
Member

Perhaps something that contributes to confusion is that the new syntax (without quotes) is not distinguished from the old (with quotes) by a different token/delimiter. It is only different in that tokens (the quotes) are omitted. Adding a new (leading?) token may look strange, but catches the eye and makes very clear that a new style is in play. import @built_value/serializer or something similar.

I think the quotes are characters that the brain doesn't really catch on. I'd have to look twice to see whether an import directive is using them or not.

@Levi-Lesches
Copy link

Levi-Lesches commented Oct 26, 2021

I actually find it's simple to understand, but it would really simplify things if we kept package imports and file imports seperate: package imports use the new proposal, and file imports use the old quoted syntax.

// Assuming we are in lib/src/logic.dart of a package called my_package
import built_value;             // 1. "package:built_value/built_value.dart"
import built_value:serializer;  // 2. "package:built_value/serializer.dart"
import :data;                   // 3. "package:my_package/data.dart"
import "../data.dart";          // 4. Matches current behavior, imports lib/main.dart like #3
import "firestore.dart";        // 5. Matches current behavior, imports lib/src/firestore.dart
import "data/user.dart";        // 6. Matches current behavior, imports lib/src/data/user.dart

This makes sense using the following rules:

  • When using quotes, the behavior matches the current import behavior
  • When omitting quotes, the syntax a:b means a is the package and b is the path.
    • The default package name is the current package
    • The default path is the name of the package (you can also omit the :)

Technically, I suppose that means import :; should import "package:my_package/my_package.dart";, but that should be disallowed/discouraged because it looks terrible (I've never even had to do that before). But besides for that, I think limiting the syntax to a:b for package imports and quoted strings for file imports makes the most sense, both matching what users are already familiar with while intuitively shortening some of the longer imports.

EDIT: Regarding the difference between dart run a:b and import a:b, I think the distinction is quite intuitive. In both cases, users are saying the same thing: Give me the code b from package a. In the case of dart run that means in bin/ and in the case of import that means in lib/, but those details aren't important. Nobody "runs" a lib/ file, and nobody imports a bin/ file, so Dart can choose the right one by itself and not bother the user with making that choice.

@natebosch
Copy link
Member

I do worry about overloading the package:binary from dart run with package:library for an import, but I'm not worried about the rest.

I'm not worried about making code less readable - I think we're stripping away noise which should make it more readable. It's a high cognitive load to answer the questions

I'm not worried about introducing too much choice for users - I think we can, and should, provide a single recommended pattern to use. I think the only "new" choices we are introducing are whether to use the old syntax or new (I think we recommend to use the new syntax) and whether to use package:path or :path for same-package imports (I think we recommend to use the latter).

The choice that's harder for us to give a universal recommendation on is package vs relative imports in the same package - and that choice is not changing with this proposal.

I don't think the new syntax will have a higher cognitive load in the long run. It's a high cognitive load to think about exactly how to translate to the old syntax, but not necessarily a high cognitive load to understand what library the syntax represents.

@TimWhiting
Copy link

TimWhiting commented Oct 26, 2021

I personally don't think it is a high cognitive load, since the difference between package / relative imports already exists, the only change is that this gets rid of some of the noise. The only concern I see is the pub run syntax.

However, pub run already has a subcommand sort of syntax (though I guess this is probably just an argument parser in the executable)
for example

pub run build_runner build

Why not deprecate the pub run package:executable syntax and encourage the subcommand syntax instead of building the language around the external tool? It is only a visual conflict, and shouldn't affect anyone during the migration period since dart run is what looks in the lib folder, and pub run looks in executable folders.

@rrousselGit
Copy link

rrousselGit commented Oct 26, 2021

Personally, I don't see the value in the proposal.

With IDE integration, I neither need to read nor need to edit imports.
The IDE will automatically add/remove/sort them for me.

Even when there is a name conflict (which is rare), it's not the imports that I will look at, but the compiler error message which says "Class A is defined in both package_a and package_b".

The only thing I really care about is exports. But exports primarily use relative file paths.

@munificent
Copy link
Member

munificent commented Oct 26, 2021

Is this really worth the shorter syntax?

Yes, I strongly believe it is. From the day we introduced pub to Dart, I have wanted a better import syntax. It's always been bad. Here's how you idiomatically import SomeUsefulThing in various languages:

import SomeUsefulThing                                      // Swift
import org.cool.SomeUsefulThing                             // Kotlin
import org.cool.SomeUsefulThing;                            // Java
using CoolOrg.SomeUsefulThing;                              // C#
import { SomeUsefulThing } from "./SomeUsefulThing";        // TS/JS
import some_useful_thing                                    # Python
require 'some_useful_thing'                                 # Ruby

Here's Dart current and proposed:

import 'package:some_useful_thing/some_useful_thing.dart';  // Dart now
import some_useful_thing;                                   // Dart shorthand

Our current syntax is really verbose, even worse than JavaScript, which is saying something. We are the only language that requires a file extension in every import (!). We're the only one with essentially two keywords (import and package) that you have to write when importing from a package. Most don't require quotation characters. In the common case of a package's name being the same as its main library, you have to say the same name twice. It's bad.

I really worry that we're making code significantly less consistent and readable in the name of brevity.

The inconsistency will be temporary and a fix is easily automatable. It would be less inconsistent if we'd designed a good syntax to begin with, but here we are.

I don't think it's less readable. I think having to squint past the boilerplate "package:___.dart" to find the actual thing being imported is harder to read.

This is an extraordinary amount of new cognitive load for a user. There are now somewhere around 8 ways to write an import of serializer.dart, some of which are only valid in certain locations,

Don't forget ./serializer.dart, ././serializer.dart, ../../../back/into/dir/serializer.dart, etc. :)

I agree there are a lot of potentially different ways to refer to libraries but for any given library the guidance is pretty simple. It's:

  1. If you're importing a library in another package:

    1. If the library has the same name as the package, use import name;.
    2. Else, use import package_name:dir/name;.
  2. Else, if you are importing a library with the same top-level directory (i.e. you are not reaching into or out of lib, bin, etc.), use import ./relative/path;

  3. Else, use import :dir/name;

Compare that to the current rules:

  1. If you're importing a library in another package, use import 'package:package_name/dir/name.dart';.

  2. Else, if you are importing a library with the same top-level directory (i.e. you are not reaching into or out of lib, bin, etc.), use import './relative/path.dart';

  3. Else, use import '/dir/name.dart';

The only additional complexity is the shorthand for a package whose name is the same as the library, but that rule also avoids a very common source of redundancy.

Users will have to be aware of both the old and new forms during the migration, which is a drag. I think it's worth the transition.

and all of which use overlapping syntax to mean different things (":something means the package part was elided, but foo:something means add the package: back in, change the : to a / and add a .dart").

I think it's simpler to understand the semantics directly and not in terms of a desugaring to the old crappy syntax:

  1. If the import starts with ./ or ../, then it imports the .dart library at that path relative to the current library.
  2. If it's of the form foo:some/path, then it imports some/path.dart from package foo. (There also happens to be a magical built-in package named dart.)
  3. If it's of the form :some/path, then it imports some/path.dart from the current package.
  4. If it's of the form foo, then it imports foo.dart from package foo.

(There is also a special rule for handling dotted package names, but that's only meaningful for internal users.)

I'm personally not too attached to (3), and would be OK with ditching the :foo syntax.

There's numerous ways to write the same thing:foo and foo:foo and package:foo/foo.dart

There's numerous ways to create a list of integers too, but as long as there's a clearly better idiomatic one, it doesn't seem too onerous to me.

and :./foo and .foo and "./foo.dart" and "foo.dart" all might mean the same thing (I think? I'm a little lost in the weeds.).

Only the last two of these are valid, and those are both valid today.

@leafpetersen
Copy link
Member Author

Here's how you idiomatically import SomeUsefulThing in various languages:

import SomeUsefulThing                                      // Swift
import org.cool.SomeUsefulThing                             // Kotlin
import org.cool.SomeUsefulThing;                            // Java
using CoolOrg.SomeUsefulThing;                              // C#
import { SomeUsefulThing } from "./SomeUsefulThing";        // TS/JS
import some_useful_thing                                    # Python
require 'some_useful_thing'                                 # Ruby

How many other ways are there to import things in each of those languages?

@TimWhiting
Copy link

How much breakage would happen if the old import syntax was deprecated then later removed so that there is only one way to import?

What invalid characters are people using that would cause issues?

@Levi-Lesches
Copy link

How about leaving quotes in as the default for relative imports? That way, invalid characters can still be supported
and there would only be one way of doing things: package imports use package:path and relative imports use quotes, instead of 2 ways of doing both (it doesn't seem like relative and package imports are being merged anytime soon).

From my earlier comment, you'd have this very concise list of options:

// Assuming we are in lib/src/data/user.dart of a package called my_package
import built_value;             // 1. "package:built_value/built_value.dart"
import built_value:serializer;  // 2. "package:built_value/serializer.dart"
import :data;                   // 3. "package:my_package/data.dart"
import "profile.dart";          // 4. Matches current behavior, imports lib/src/data/profile.dart
import "../firestore.dart";     // 5. Matches current behavior, imports lib/src/firestore.dart

@cedvdb
Copy link

cedvdb commented Oct 27, 2021

Anyone should be familiar with those 3 types of imports:

  • import built_value; (although the current dart behavior is to go in the same folder, this makes more sens to me).
  • import ./src/int_serializer;
  • import ../serializer;

Those 2 are questionable, to me at least, as it's not immediately obvious what the : means:

  • import built_value:serializer; It's a bit unclear why not build_value/serializer. To me import built_value; takes the default barrel, if you don't want the default barrel you have to go down the path.
  • import :src/int_serializer; again I'd have used a token that is more common in file paths import /src/int_serializer; to signify from the root but that might be just me

The : could have a single meaning of external dependency instead:

  • import :built_value;
  • import :built_value/serializer;
  • import ./src/int_serializer;
  • import ../serializer;
  • import src/int_serializer;

or even get rid of it:

  • import built_value;
  • import built_value/serializer;
  • import ./src/int_serializer;
  • import ../serializer;
  • import /src/int_serializer;

@lrhn
Copy link
Member

lrhn commented Oct 27, 2021

We can definitely ditch the :foo syntax and use /foo instead. It would mean the same thing, equivalent to "/foo.dart" today. That makes anything starting with /, ./ or ../ a path-only reference relative to the current location/package, and anything containing id: refers to a package.

I was once convinced by others that :foo was more understandable than /foo (don't remember why any more), but obviously opinions differ.

As for why not build_value/serializer: I actually want to make the package name distinguishable from the path.
If I'm inside "package:build_value/serializer.dart", known by the path build_value/serializer, and I do ../../collection/collection, what does that mean? (Answer: By the URI rules it means package:build_value/collection/collection.dart, because the package name is not part of the hierarchical path, it's an outside selector more like if the original URI had been package://build_value/serializer.dart, and that's how the Dart URI class treats package URIs).
So, I prefer to not use / for something which is not part of the path. Putting something significant in front of the package name is another option, like //package_name or @package_name, but it's more verbose that just package_name and package_name:path.

@lrhn
Copy link
Member

lrhn commented Oct 27, 2021

About the cognitive load, I think people will quickly learn the basic rules:

If you omit the quotes, then the import must start with one of:

  • a package name (when importing from another package),
  • dart: (importing a platform library), or
  • one of /, ./ or ../ (path importing from the same package).

If the library you import from another package is not the main library (same name as package), you use package-name:library-path.

(For experts only: Package names containing .s.)

(Switched :path to /path here. Cognitive-load-wise, I think it fits better with ./ and ../ as a single case).

@lrhn
Copy link
Member

lrhn commented Oct 27, 2021

Technically, the Dart language allows any URI as import. Only package: and dart: URIs are guaranteed to work, because they are actually part of the language and mentioned in the language specification.

Our tools (compilers included) support loading files from other URI schemas than those two, currently probably only file:, http: and https: (I haven't actually checked).

The import shorthand will only apply to package:, dart: and relative paths. That should cover 99.999% of actual imports. It still resolves to an actual URL before being loaded, it's just a shorthand for some (very vast majority) of URIs.

It also just applies to paths containing only ASCII letters, digits, dots and underscores, which again applies to almost all actual imports (I'm open to adding - to the list if necessary).

@lrhn
Copy link
Member

lrhn commented Oct 27, 2021

The compiler will know exactly waht to do.

If you write import http:http; it's the same as import http; and import "package:http/http.dart";.

Import shorthands, without quotes, cannot specify anything other than package: URIs, dart: URIs, or relative paths. If you need a file: or http: schemed-URI (which is not just relative to the current library's URI), you need to use the "old-style" quoted syntax.

Unless you're writing stand-alone scripts, with no surrounding Pub package, that won't happen. Even those will likely use relative imports 99% of the time.

@cedvdb
Copy link

cedvdb commented Oct 27, 2021

As for why not build_value/serializer: I actually want to make the package name distinguishable from the path.

I'm confused. Isn't the package name the first value in the path (here built_value) , or is it possible for that not to be the case ? I assumed the the current way of importing a local file from the same directory would need a ./ in front from here on out.

We also know it's a package because it does not start with one of: ./, /, ../.

If I'm inside "package:build_value/serializer.dart", known by the path "build_value/serializer", and I do ../../collection/collection, what does that mean?

What does "I'm inside" and "I do" mean in this case ? I parse this as "I write import ../../collection/collection in the build_value/serializer.dart file" which makes no sens as it's going outside the lib.

@lrhn
Copy link
Member

lrhn commented Oct 28, 2021

@cedvdb I agree that it makes no sense and you are going outside "the lib directory", but there is no "lib" visible in what looks like a path. That's why I prefer to separate the package name from the hierarchical path with a character other than /, because it's not like the other /s in the path.

It can certainly work to use /. The syntax is unambiguous.

You do import build_value/serializer; or import foo/bar/baz;, but if, in the latter library, doing an import ../../../qux will ... either be an error (my preference) or just mean import foo/qux;.
The first slash is special, it's not like the other slashes, but it doesn't look special.
That's what I want to avoid by using : instead. (That, and making dart:async stay the same as now, while following the same syntactic pattern, looking like it's a "dart" package, but that's just a bonus.)

@lrhn
Copy link
Member

lrhn commented Oct 28, 2021

@tatumizer You do know to push where it hurts, don't you 😝

Yes, that one annoys me too. It looks like it should be allowed. It's not, it is an error.

It's currently not allowed grammatically. The grammar is very restrictive and says something like (from memory):

importShorthand ::= packageName (: path)? | relativePath
path ::= pathSegment (/ pathSegments)*
relativePath ::= ./ path|../+ path|/ path`

It's not a fully general "anything goes" grammar. You can't have embedded /./ or /../ path segments (only one leading ./ or / or a number of leading ../s), no adjacent //s, etc. And no packageName:/path.

I do consider import /foo; a very likely alternative to import :foo;. (I've been going back and forth between the two, leaning towards / today).

@leafpetersen
Copy link
Member Author

I do consider import /foo; a very likely alternative to import :foo;. (I've been going back and forth between the two, leaning towards / today).

FWIW this feels less confusing to me as well. If I understand it correctly, with that model I really only need to think about two kinds of abbreviations:

  • "Path imports". Either "absolute" (.e.g using import /foo) or relative (import ../foo)
  • "Package imports". General form is somepackage:filepath but can just use somepackage as a shorthand for somepackage:somepackage.

Technically, there's still the old syntax for things that require escaping, but that's ok, you only use those for things that require escaping.

Is that the intuition for dummies like me?

@lrhn
Copy link
Member

lrhn commented Oct 29, 2021

@leafpetersen No, that sounds exactly right. And you can write import dart:async; as if dart was a package.

(Or you can think of it as separate, but similar syntax for platform libraries only. In the long run, people will probably start to think of dart as an implicitly available package, and that's fine with me.)

@lrhn
Copy link
Member

lrhn commented Oct 29, 2021

Now I remember the reason for preferring : as the "current package" identifier: It can be made to work for files in test/ and bin/ of a Pub package too without being weird.

So, if import /path is just equivalent to import "/path.dart", and import :path is equivalent to import current_package:path, then inside lib/ (in a file with a package: URI), they work the same.

Then :path only works on files "inside a package", but /path "works" everywhere (in that it has a well-defined meaning, just not one you'd ever want to use.)

Outside of lib/, the :path works, and is meaningful when writing code in test/ and bin/, and /path probably doesn't work for anything. You likely have a file: URI for the surrounding file, and you are very, very unlikely to want to specify an import starting from the root of your file system. You'll always want to use relative paths for that.

We can allow both syntaxes, but that risks someone using /path in bin/ instead of :path, because the difference is so subtle. That's bad.
Then we can disallow using / in non-package: files. That's probably a good idea, but reduces its power significantly.
That makes the :path functionality a strict superset of /path, they do the same inside lib/, which is the only place a /path import is allowed, and :path works in the rest of the Pub package as well.

That suggests just having one of them, which again points to :path as the more general one.
Or allowing both, and recommend using /path inside lib/ and :path outside of lib/ (but allowing :path inside lib/ as well, if you want to.)

Really, /path has nothing going for it except that it reads better as a path instead of a package reference.

I'd like to be opinionated with this design (if you do something non-idiomatic, use a string URI instead), and it irks me to have two ways to do the same thing. And it also irks me to use :path where /path seems more right (to me, which might be the problem: it's heavily subjective).

WDYT?

@jakemac53
Copy link
Contributor

I think it would be very weird if / referred to the package uri root (usually lib, but it doesn't have to be!). I would be sort of ok with it referring to the actual root of the package (ie: next to the pubspec.yaml), but that is a lot less useful also. I think it is probably best to just avoid it.

@jakemac53
Copy link
Contributor

Do you mean that the situation is less weird with import 'package:built_value/serializer.dart'; than it could be with simply import "/serializer.dart" ? To me, it looks like the same thing.

Yes, to me / very clearly means either the actual root of the package (ie: where the pubspec lives), not the "package uri root" ie: where package: uris resolve to. It would be highly confusing if it resolved to the package uri root to me.

@Hixie
Copy link

Hixie commented Oct 29, 2021

FWIW, I 100% agree with the concerns that there's too much complexity here.

I would go with only four forms (two of which exist today):

import '../relative/path.dart'; // relative paths are strings and this works fine today
import 'package:foo/bar.dart'; // package imports can give precise paths and work fine today
import foo; // shorthand for importing "package:foo/foo.dart", most intuitive syntax, matches other languages
import foo.bar; // shorthand for importing "package:foo/bar.dart", matches other dot-delimited features in Dart

The non-quoted forms would be either or ., nothing more complicated. This makes parsing it trivial and unambiguous to readers, and avoids introducing yet another kind of token for people to understand.

@natebosch
Copy link
Member

. should not be the separator. It is a valid character in a package name (not in pub) and it's how we map to hierarchical packages in bazel.

@Hixie
Copy link

Hixie commented Oct 29, 2021

I think . has to be the separator if we have one (we don't have to have one, we could just not support importing libraries in this way with the new syntax).

We shouldn't let bazel affect how we design the language, IMHO.

@Hixie
Copy link

Hixie commented Oct 29, 2021

(All the popular languages that support a way to do this use periods, none of them use colons. I think that's an overwhelming argument in favour of periods.)

@munificent
Copy link
Member

  • "Path imports". Either "absolute" (.e.g using import /foo) or relative (import ../foo)
  • "Package imports". General form is somepackage:filepath but can just use somepackage as a shorthand for somepackage:somepackage.

I think a better mental model is that leading "/" or ":" (whichever syntax we choose) are package imports, not path ones. So it's basically:

  • Path imports. Always relative starting with ../ or ./.
  • Package imports. General form is somepackage:filepath, but can just use somepackage as shorthand for somepackage:somepackage, and can omit somepackage if it's the current package. (But can't apply both shorthands because import :; is hilariously weird.)

@lrhn
Copy link
Member

lrhn commented Nov 12, 2021

@tatumizer I wouldn't expect any relative imports starting with ./.

The only character I've found in file/directory names other than [a-zA-Z0-9_.] (no $!) is -. Adding that seems reasonable, which is why I did so in the latest revision of my proposal.

@lrhn
Copy link
Member

lrhn commented Nov 12, 2021

@lrhn: I can't believe you like this grammar.

Believe it! :) Or rather, ignore the grammar, look at the language. The grammar is an implementation detail.

For something non-quoted, I want a measure of regularity.
It's basically [a-zA-Z0-9_]+ chunks which can be separated by . or - (so no leading, trailing or adjacent ./- characters).
Then those "segments" are separated by / for paths and : between package name and path.

That covers all existing package names, directory names and file names in the Dart imports I checked.
So, it's not like the result of the grammar is new, it's what everybody is doing already. It's not a new syntax. It's the language of package and file-system names that has already evolved, and this is just one possible grammar codifying what is already happening.

We could allow more, but at this point, I'm actually being opinionated and saying that you should stay inside that language if you want to use the quote-less imports.

And yes, dots are special in that:

  • In paths . and .. are special.
  • In package names, I do care about supporting dotted package names the way they are already being used in hundreds of thousands of existing files. I'm not trying to invent something new here.
  • In path names, a do care that some files are called foo.g.dart.

Maybe we should invent something new, from scratch, instead of this rather incremental approach, but I actually think it's a nice increment.
I have no urge to invent a completely new way to designate libraries, different from the URL. I just want a shorter way to write the URL, one which is compatible with as many existing imports as possible.

@lrhn
Copy link
Member

lrhn commented Nov 15, 2021

@tatumizer

Now, suppose we are in a file a.dart under test directory, and we need to import b.dart from lib/src directory.
There are 2 ways to do it:
import '../lib/src/b';
import '/lib/src/b';

Both of those are wrong ways to do it. Files inside the lib/ directory should (going on must) be referenced using a package: URI. So, unless we recognize relative paths ending up inside the lib/ directory of the current package (or not just the current package?), and rewrite them to being package URIs, it's going to give a wrong result. (The wrong result is the same library imported both with a package: URI and a file: URI, meaning it's treated as two different libraries).

If you also want to change how Dart recognizes and canonicalizes library import paths, and how it determines whether two different URIs represent the same library, we can do that too, but that's a bigger feature than just doing shorthands for the current URIs. This is considered a "small feature" because it's not changing anything about resolution, it's literally just shorthands for URI references.

Package names: Dot-separated package names look more familiar (and arguably nicer) than the alternatives (slash- or colon-separated).

You probably mean "library names" there. Currently, all Pub package names are simple identifiers, but I don't actually believe that will scale forever. I personally expect that we'll allow .s in Pub package names at some point, to widen the namespace. I could be wrong, maybe underscores can be used instead, so you'd do myproject_mypackage instead of myproject.mypackage. Or maybe people will just have one package for their project, with multiple top-level libraries, to avoid namespacing.
(What if Pub allowed .s in package names today. Would your preferences change? Why/why not? Would you want it to?)

That said, if we did that, I also think Pub should allow aliases/renaming for packages, so you can depend on foo.bar.baz as baz, and do imports as package:baz/... (with package-local package-names).

I can see how foo.bar.baz reads "cleaner" than foo:bar/baz, almost as if it was a name, not an unclearly separated package name and a path into that package. It is a package name and path, though. Maybe if it was both?
What if we allowed you to import foo.bar.baz only if the target library (package:foo/bar/baz.dart) was actually named so, using library foo.bar.baz;. Then the author of a package can mark the libraries that are intended for external import by giving then the name corresponding to their path, and effectively make every other library package-private. (And it would give library names a reason to exist again).
That would still be an indirect way to introduce package-private files, and I do prefer explicit to implicit when it comes to "enabling" features, it's a pain to enable some use of your library by accident, and then not be able to remove it again without breaking customers.

@lrhn
Copy link
Member

lrhn commented Nov 16, 2021

The thing is that if a.b.c is a library name, with an implied package name of a, and path of b/c.dart, then a.b doesn't have to be anything.
I don't see the .s as hierarchical, with that view it's just a dotted name which happen to match the path of the library with that name. If a.b.c exists, it doesn't mean that a.b is a library too, there does not have to be a library with path package:a/b.dart, or if there is, it doesn't have to have name a.b, and in either case, you can't import it as import a.b.

It's still a semi-magical link between package/path and dotted names, but all it means is that libraries without names, or with names that doesn't match their path, cannot be imported by their name. They can still be imported using their URI path.
(And we can then, potentially, disallow importing from other packages by anything but name, which effectively makes every library package private if it doesn't have a library a.b.c; declaration matching its package name and path.)

TL;DR: That idea, entirely separate from what we are doing here, would be to allow you to import packages by their name iff their name is their package name and path segments, without the trailing .dart, separated by .s. (And only if those package names and path names contain no . characters.). It's different. Not necessarily bad.

@lrhn
Copy link
Member

lrhn commented Nov 16, 2021

The problem here is that the compiler, seeing pkg.a.b.c.d.e must try all 16 possible interpretations in order to see if there is a file, and it can't even stop at the first one it finds if it has to check for ambiguities. That's certainly possible, and with a little look-ahead (if there is no a.b directory, we don't need to check for the ways c.d.e can be separated), it probably won't be bad in practice. You'd have to go out of your way to get actual exponential behavior. It's still more expensive than just finding the file directly, and you have to do it for every import in the program. (You'd even have to be careful about caching the result, because creating a new file somewhere else might change the result.)
If you use / for actual path separation, and allow . in names, then there is no ambiguity.

@munificent
Copy link
Member

munificent commented Nov 18, 2021

OK, here's another pitch:

import dart/isolate;                      // -> 'dart:isolate';
import flutter_test;                      // -> 'package:flutter_test/flutter_test.dart';
import path;                              // -> 'package:path/path.dart';
import flutter/material;                  // -> 'package:flutter/material.dart';
import analyzer/dart/ast/visitor;         // -> 'package:analyzer/dart/ast/visitor/visitor.dart';
import widget.tla.server;                 // -> 'package:widget.tla.server/server.dart';
import widget.tla.proto/client/component; // -> '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';
import 'user_address.g.dart';

The rules are:

  • If the import path is a quoted string, it behaves exactly as it currently does.

  • Otherwise, the path may be a slash-separated series of dotted identifiers.

    1. If there is only a single path component (i.e. one potentially dotted identifier but no /), then it is treated as a package import for a package with that identifier and library with the last dotted component of that identifier suffixed with .dart.

    2. Else, the first path component is treated as the package name, the rest is the library path, and .dart is added to the end.

  • The name dart is reserved as a magic package containing the built-in libraries.

That's it.

In other words, it's the original proposal but with no provision for unquoted relative paths, no rule for the current package, and / instead of : for the package name separator.

It doesn't support unquoted relative imports because:

  • Fewer ways to express the same thing. Package imports are the really painfully verbose ones. The benefits of an unquoted syntax for relative imports are marginal, so it's good to avoid the cognitive load and confusion for having two ways to express those.

  • Focusing only on package imports still covers roughly 2/3s of imports.

  • If you really want an unquoted import in your own package, you can simply use an unquoted package-style import (except in the rare cases where you want to import something not in lib/).

  • It keeps imports of generated files like .g.dart from looking very strange.

It uses a slash as the package name and library path separator because:

  • That lets us support packages with dotted names. This is critical internally today, and may be useful in the future if we ever want to add namespaces for pub packages (which has been a user request literally since before Pub was launched). If we use an import syntax that doesn't handle dotted package names, we are painting ourselves into a corner as far as ever supporting namespaces in the future.

  • It's closest to the current syntax. That makes it easier for existing Dart users to get used to reading it. The proposed syntax is literally already embedded in the middle of every existing package import.

  • Likewise, it makes it easy to migrate: just shave off the 'package: and .dart' and you're done.

  • The thing you are importing is a path physically inside the package, so a familiar path separator character makes sense. It communicates that you are importing a thing from within the package.

Aesthetically, I think it looks pretty nice.

@Hixie
Copy link

Hixie commented Nov 19, 2021

Here's what some files would look like:

https://github.com/flutter/flutter/blob/master/packages/flutter/test/widgets/layout_builder_mutations_test.dart:

import flutter/src/rendering/sliver;
import flutter/src/widgets/basic;
import flutter/src/widgets/framework;
import flutter/src/widgets/layout_builder;
import flutter/src/widgets/scroll_view;
import flutter/src/widgets/sliver_layout_builder;
import flutter_test;

https://github.com/flutter/flutter/blob/master/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart::

import dart/async;
import dart/io as io;

import args/command_runner;
import file/memory;
import flutter_tools/src/base/common;
import flutter_tools/src/base/error_handling_io;
import flutter_tools/src/base/file_system;
import flutter_tools/src/base/io;
import flutter_tools/src/base/signals;
import flutter_tools/src/base/time;
import flutter_tools/src/build_info;
import flutter_tools/src/cache;
import flutter_tools/src/pub;
import flutter_tools/src/globals as globals;
import flutter_tools/src/pre_run_validator;
import flutter_tools/src/reporting/reporting;
import flutter_tools/src/runner/flutter_command;
import test/fake;

import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/test_flutter_command_runner.dart';
import 'utils.dart';

https://github.com/flutter/flutter/blob/master/dev/bots/prepare_package.dart:

import dart/async;
import dart/convert;
import dart/io hide Platform;
import dart/typed_data;

import args;
import crypto;
import crypto/src/digest_sink;
import http as http;
import path as path;
import platform show Platform, LocalPlatform;
import process;

https://github.com/flutter/flutter/blob/master/dev/integration_tests/flutter_gallery/test_driver/transitions_perf.dart:

import dart/convert show JsonEncoder;

import flutter/material;
import flutter_driver/driver_extension;
import flutter_gallery/demo_lists;
import flutter_gallery/gallery/app show GalleryApp;
import flutter_gallery/gallery/demos;
import flutter_test;

import 'run_demos.dart';

That's not too bad. The slash isn't as satisfying as the dot for a separator IMHO, but it's clearly intuitive, and there's a path component to this, so it is sound from a conceptual point of view. It solves the verbosity problems cleanly. I could get used to the slash, I think. The syntax is simple and the mapping from this syntax to the old syntax is easy to explain.

SGTM.

@cedvdb
Copy link

cedvdb commented Nov 19, 2021

Imo something.g does not look stranger than something.utils or widget.tla.server if the rule is to assume a dart extension. I believe that's a non issue that was labeled as one.

Having 2 different rule set for relative and package imports does not make imports simpler. I don't want to have to think whether I should add the extension or not. I'd prefer dropping the extension in all cases for relative imports to keep both streamlined.

Also, and this is purely a preference, I'd prefer to use a dot for relative a la typescript A relative import is one that starts with ./ or ../. But quotes are fine too I guess, although quotes are more verbose.

import ./test_utils;
import ../util/dart_type_utilities;
import ../../../room/member/membership;
import ./src/assets/source_stylesheet;
import ./user_address.g;

@munificent
Copy link
Member

Having 2 different rule set for relative and package imports does not make imports simpler.

That's already true today. Both forms are quoted, but you still have to know about the semi-magical "package:" URL scheme.

@a14n
Copy link

a14n commented Nov 22, 2021

To refer to the current package (/lib) couldn't we use ~/ as prefix?

import dart/async;
import dart/io as io;

import args/command_runner;
import file/memory;
import test/fake;

import ~/src/base/common;
import ~/src/base/error_handling_io;
import ~/src/base/file_system;
import ~/src/base/io;
import ~/src/base/signals;
import ~/src/base/time;
import ~/src/build_info;
import ~/src/cache;
import ~/src/pub;
import ~/src/globals as globals;
import ~/src/pre_run_validator;
import ~/src/reporting/reporting;
import ~/src/runner/flutter_command;

import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/test_flutter_command_runner.dart';
import 'utils.dart';

If this prefix doesn't bring syntax problem, this proposal makes the package's renaming a no-op, makes imports of current package shorter, allows grouping current package imports and has a relatively obvious meaning imho.

@lrhn
Copy link
Member

lrhn commented Nov 22, 2021

It might be a minor point, but isn't it odd that there's no way to refer to the root of the current package explicitly [in relative import]? The above expression has some room for improvement IMO. :-)

import '/src/helper.dart';

works today (has worked since SDK version 2.13).

There is no way to refer to the root of the pub package. I can live with that. As Bob makes a point of, his proposal only supports package URIs, and outside of lib/ is not a package URI. So, that works.

About using ~/, I assume it should work everywhere in the same Pub package, as a shorthand for "current package name/".
That's not bad. Probably better than just using a leading slash if we use / as package-name/path separator. (If we use : as package-name:path separator, a leading slash is harder to mistake since slashes only occur in the path).

I actually think that distinguishing imports of the current package from other imports is a good thing for readability.
When I look above and see:

import "package:helper/helper.dart";
import "package:foo/foo.dart";
import "package:test/test.dart";

it doesn't stand out that foo is the package I'm about to test, my own package. It's just another package in the pile.

@Levi-Lesches
Copy link

I like @munificent's proposal, with the personal preference of using : instead of / after the package.

  1. The precedence comes from the existing package:foo syntax and the dart run command
  2. It does a good job of visually separating the package name from the rest of the path
  3. It would more easily allow omitting either side of the : (the package name or the path)

In other words,

It's closest to the current syntax.

and

it makes it easy to migrate: just shave off the package and .dart and you're done.

Some examples:

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.dart';
import '../util/dart_type_utilities.dart';
import '../../../room/member/membership.dart';
import 'src/assets/source_stylesheet.dart';
import 'user_address.g.dart';
import dart:async;
import dart:io as io;

import args:command_runner;
import file:memory;
import :src/base/common;
import :src/base/error_handling_io;
import :src/base/file_system;
import :src/base/io;
import :src/base/signals;
import :src/base/time;
import :src/build_info;
import :src/cache;
import :src/pub;
import :src/globals as globals;
import :src/pre_run_validator;
import :src/reporting/reporting;
import :src/runner/flutter_command;
import test:fake;

import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/test_flutter_command_runner.dart';
import 'utils.dart';
import dart:async;
import dart:convert;
import dart:io hide Platform;
import dart:typed_data;

import args;
import crypto;
import crypto:src/digest_sink;
import http as http;
import path as path;
import platform show Platform, LocalPlatform;
import process;

@munificent
Copy link
Member

In other words,

It's closest to the current syntax.

and

it makes it easy to migrate: just shave off the package and .dart and you're done.

But using a : instead of / between the package name and path breaks both of these.

@Levi-Lesches
Copy link

Levi-Lesches commented Dec 2, 2021

it makes it easy to migrate: just shave off the package and .dart and you're done.

Yeah I was wrong on that, not sure why I thought so. It does make the migration slightly more involved.

It's closest to the current syntax.

Packages are already denoted by a :. True it's not currently between the package name and the path, but the token itself is associated with having a package followed by a path. In other words, the following two transformations both have the same elements, but the : and / are recognizable/distinct to different people.

import "package:flutter/material.dart";
import flutter:material;  // IMO, represents a library in a package
import flutter/material;  // IMO, represents a path that happens to be a package

@cedvdb
Copy link

cedvdb commented Dec 2, 2021

Does the distinction have to be made between a library and a file ? Aren't they all in essence the same ?

What is the difference between a file with library at the top that exports other files, and the same file without library at the top ?
What is the difference with that same file, which instead of exporting other files had all the files content copy pasted there ?

@Levi-Lesches
Copy link

Levi-Lesches commented Dec 2, 2021

First of all, the presence of : signifies that the import is coming from a package, as it currently does. / doesn't carry the same connotation because it's used in every path.

What is the difference between a file with library at the top that exports other files, and the same file without library at the top?

When I say library, I'm not referring to the library foo; statement on the top of the file, nor is there any technical difference in a file that has one and one that doesn't. It's about how the importer of the package thinks about what to import. This has already been brought up, so I'll just briefly describe what I mean using an example from Flutter.

When you import your own files in your own package, you do so using a relative import, like import "auth.dart";. The intention is to import a file in the same folder that has functions/classes to help with authentication.

But when writing the UI, you want to import Flutter, specifically the Material components. In that case, you don't want to focus on a specific file from Flutter, because you don't care how it's organized. You just want to say "give me all the Material components from Flutter".

The current way of importing feels like you need to know a bit of the internals of Flutter, because you're spelling out a path: import "package:flutter/material.dart";. This literally means "go to the flutter package folder, and import the file named material.dart". Instead, it would be nice if we could make it import flutter:material;, which would more accurately represent the idea of grabbing all Material-related libraries/classes/functions from Flutter. This is closely related to dart run package:executable, which doesn't concern itself with lib vs bin, and instead says "get me the executable from package".

Of course, this is all possible because Flutter has a file called material.dart in the flutter folder that exports all the related Material material. But the user shouldn't have to know or understand that to use it.

@cedvdb
Copy link

cedvdb commented Dec 2, 2021

So it's to communicate the intent. That begs the question why does this have to be reserver for the root of a package then ? If I have a nested models/models.dart file that exports all my models it's essentially a library so shouldn't there be a : somewhere ?

Given that you wrote this:

import flutter:material;  // IMO, represents a library in a package
import flutter/material;  // IMO, represents a path that happens to be a package

The following does not make much sens to me:

import `../:models`;

but I don't see why it would not be valid given your description

@Levi-Lesches
Copy link

Levi-Lesches commented Dec 2, 2021

If I have a nested models/models.dart file that exports all my models it's essentially a library so shouldn't there be a : somewhere?

Correct. Let's make this concrete; I structure my projects like this:

my-project/
- lib/
  - main.dart
  - models.dart
  - data.dart
  - src/
    - data/
      - dataclass1.dart
      - dataclass2.dart
    - models/
      - model1.dart
      - model2.dart
- pubspec.yaml

Here's what my lib/models.dart file looks like:

// models.dart
export "src/models/model1.dart";
export "src/models/model2.dart";

So, if you wanted to import my-project/lib/src/models/model1.dart, as in your comment, you would simply do:

// main.dart
import "package:my-project/models.dart";
// or 
import :models;

The following does not make much sense to me:

That's because you're mixing up the ideas of relative imports and package imports. My comment is only about importing from other packages, since you don't know how those are laid out and using the top-level (non-src) files are kind of like using an API. When it comes to importing from your own package, you know the entire layout of the files. I would use a package import like I described above when possible, but plenty of times you just need to import a single file, and that's where you would use a relative import.

@lrhn
Copy link
Member

lrhn commented Dec 2, 2021

I fully believe that the precise syntax is not very important. That's probably why the discussion is so vicious 😁

I believe that Dart authors will quickly get used to any of:

  • import packagename/path/library;
  • import packagename:path/library;
  • import packagename.path.library;

The driving force behind the discussion about these is not usability, it's aesthetics. That's incredibly subjective.

The . version has practical problems (there are package names with .s in them already). Most people don't see that problem, but it's there.

For the other two, the preference seems to be driven by whether one sees packagename/path/library as a single opaque designator (which the . version definitely does), or as a path. If it's a path, then the first / is not like the others, and it's easier to argue for : instead. If it's one opaque library designation, then there is no need to have separate separators.

Perception and aesthetics based on that perception.

If we introduce a shorthand, we'll have to pick one approach, one perception, and make that the official one.
And again, I fully believe that whichever we choose, people will use it and like it (and some will complain that it doesn't match their expectations, no matter which one we choose).

@cedvdb
Copy link

cedvdb commented Dec 2, 2021

import :models;

What if the models folder was nested in a core library / folder ? so src/core/models/models.dart where there exist a src/core/core.dart file that exports :models

Would it be

import :core:models
// or
import :models
// or both are valid ?

if import :models is ever valid it solves the alias issue.

Which has usability implications and that is not entirely subjective

@Levi-Lesches
Copy link

Levi-Lesches commented Dec 2, 2021

@cedvdb I think you're extrapolating way too much from my comment. Pub conventions are that importable code should be directly in your lib folder and implementation files go in lib/src -- I'm not trying to change that. So regarding your example, you shouldn't be exporting publicly importable files in src, because people shouldn't be importing from src (and if you tried to, you'd get a lint telling you not to).

My proposal to use : is based on the current convention: if you have a package named myPackage with an importable file (in lib) named library, you would import it using myPackage:library. If for some reason you need to import beyond lib -- in other words, you're no longer importing what the package author intended and are using your own knowledge of the package's file structure -- you should use a path-like syntax to indicate you're trying to get to a specific path. So if you have a package named myPackage, and you want to dig into src/models/model1.dart, you would do myPackage:src/models/model1.dart (maybe omitting the .dart).

@lrhn, I think there's no reason to choose between : and /; both have their uses. : is already used for reaching into packages (with package:myPackage), and / is known for reaching into directories. We can use both like I described above to keep that distinction clear.

@munificent
Copy link
Member

For those following along, I took this comment and fleshed it out into a full proposal here.

@Wdestroier
Copy link

Should the import 'package:widget.tla.server/server.dart'; = import widget.tla.server; rule be kept? I don't see how it adds any value. Dots are the directory and file name separators in the import syntax of some programming languages. Another reason to remove this rule is because sneak_case is the naming convention for Dart packages on pub.dev.

@ghost
Copy link

ghost commented May 16, 2024

There was a proposal to allow

`something-that-if-not-an-identifier` // in backticks 

to serve as a valid identifier.
If it gets implemented, then

import a.b.c/dartFile 
// becomes
import `a.b.c`/dartFile

and, more generally, every "unconvertible" case (even containing the allowed Unicode characters) could become "convertible" automatically.

@natebosch
Copy link
Member

natebosch commented May 17, 2024

I don't see how it adds any value. Dots are the directory and file name separators in the import syntax of some programming languages.

That's the purpose they serve here too, it's just that we don't have any public facing package organization tools that use it that way. The dart pub tooling supports only a flat package namespace. The rest of our tooling allows dotted package names. In practice this flexibility is only used by our (internal) integration with the bazel build system.

@Levi-Lesches
Copy link

In fact, every "package:" import today literally contains the proposed syntax inside its import string:

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

import 'package:analyzer/dart/ast/visitor/visitor.dart';
import          analyzer/dart/ast/visitor/visitor      ;

import 'package:widget.tla.proto/client/component.dart';
import          widget.tla.proto/client/component      ;

This strongly suggests users will have no trouble reading the syntax.

While I did find this compelling, there's a slight nuance. The keyword package: separates package/library from relative/path.

import flutter/material;
import "src/material.dart";

Now, there are the quotes and the .dart, so I don't see this as a major issue, but I do still think it would help readability to be able to denote which package you're using separately from which file within the package. This would be especially helpful for someone scanning import statements for use of a specific package (say, something they want to remove from the pubspec):

import dart:convert;
import flutter:material;
import otherPackage:something/inside;

import "src/material.dart";
import "src/something/deep/inside.dart";

@rrousselGit
Copy link

rrousselGit commented May 17, 2024

Personally I'm in the team that:

I don't think many people actually care about imports. IMO for most people, they'd likely rather have everything imported by default, and not have to type imports.
Some functional languages kind of do that already.

So to me, working on the syntax for typing imports is wasted effort. I personally only care about them in the case of a name conflict. But the IDE could highlight imports with symbol conflicts when that happens.
For everything else, the IDE already adds/removes imports automatically when needed.

@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 import-shorthand unquoted-uris The unquoted URI feature
Projects
None yet
Development

No branches or pull requests