dart-lang / language

Design of the Dart language
Other
2.64k stars 199 forks source link

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

Open leafpetersen opened 2 years ago

leafpetersen commented 2 years ago

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

Levi-Lesches commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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

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

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 commented 2 years ago
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 commented 2 years ago

@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 commented 3 months ago

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

Wdestroier commented 3 months ago

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.

tatumizer commented 3 months ago

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 commented 3 months ago

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 commented 3 months ago

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`.  
```dart
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 commented 3 months ago

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.