dart-lang / language

Design of the Dart language
Other
2.65k stars 201 forks source link

Library Supports Feature X #1144

Open TimWhiting opened 4 years ago

TimWhiting commented 4 years ago

Problem Statement

In the packaging ecosystem there is a problem around supporting different features.

For example in Flutter, there are packages that create a certain philosophy or design pattern (e.g. flutter_hooks). Then when creating some sort of convenience package for example for state management, you might have to create 3 packages:

  1. foo_core (might be reusable outside of flutter such as in angulardart, or server)
  2. foo_flutter
  3. foo_hooks
  4. ... any other design pattern you want to support, angular package ...

This requires a lot of imports for anyone using foo. Sure if they just use foo_hooks, foo_hooks can export foo_core, but if they also use some features of foo_flutter they have to import that too. To be honest it is bearable, but when you depend on multiple such packages it gets cumbersome. And the bigger problem I see is that it pollutes pub.dev as well, with a bunch of bar_hooks, bar_core, and bar_flutter packages, and you also have to make sure you import the correct versions of those packages that work together. On the package maintainer side it also becomes a burden, since you now have to worry about versioning, and uploading your 3 packages, even though some of the packages might only be one or two files, that add just a tiny bit onto the core.

Solution

I can see two solutions that might help alleviate the problem in some way:

Provide some sort of 'feature' mechanism where a library can declare which features it supports, and you can specify which imports are available to each library.

This would be the ideal solution. The author would create one package foo. The author would then create the main core content in a library called foo_core. When he wants to support flutter or hooks or streams or whatever it might be, he creates a new library within the same package.

#feature(flutter)
library foo_flutter;

And in the pubspec.yaml:

features:
  - flutter:
   dependencies:
      flutter:
        sdk: flutter
  - ... other features

These conditional 'feature-based' dependencies would only be resolvable in the library that enables that feature.

When using a package you only see libraries within that package that you have all the required dependencies for.

Pros:

Cons:

Deal with it, but maybe provide some way to group pub.dev search results using some key in the yaml file.

package: foo_hooks
packagefamily: 
  - foo_core
  - foo_flutter

This creates other questions around whether all those packages have to include each other in their package family. You probably should be a maintainer on all 3 packages as well, so people don't abuse the key.

Pros:

Cons:

lrhn commented 4 years ago

I've read this a few times, and I'm still not sure I understand what is being asked for, or what a "feature" really is. Possibly because it's talking about something that the current Dart library system does not have any concept of at all, so the words are all new.

Could you perhaps summarize it as just what the language change is (syntax) and what it does (semantics), as separated from the motivating examples? Or a more concrete example where it's more obvious what "foo" is?

dz4k commented 4 years ago

If I'm not mistaken, this feature request is kind of similar to conditional imports. So in addition to

import "foo.dart"
  if (dart.library.io) "foo_io.dart";

we would also be able to check for the presence of other libraries or packages:

import "foo.dart"
  if ("package:flutter") "foo_flutter.dart"
  if (dart.library.js) "foo_web.dart";

@TimWhiting is this close to what you're asking?

rrousselGit commented 4 years ago

I think what is described here is a solution to how some packages have to be split in multiple pieces because some dependencies are optional.

For example, I have a package named Riverpod, which is split into:

It's a bit confusing to have to split these into 3 packages when they are effectively one framework

From my understanding, this issue is asking a solution to this problem. By potentially allowing to conditionally export some objects:

export "riverpod.dart"
if 'package:flutter'
  export "flutter_riverpod.dart"
if 'package:flutter_hooks'
  export "hooks_riverpod.dart"
eernstg commented 4 years ago

Configurable URIs in exports are allowed:

// Library 'n022_vm_lib.dart'.
void foo() => print("vm foo");

// Library 'n022_dart2js_lib.dart'.
void foo() => print("dart2js foo");

// Library 'n022_lib.dart'.
export 'n022_dart2js_lib.dart'
    if (dart.library.io) 'n022_vm_lib.dart';

// Library 'n022.dart'.
import 'n022_lib.dart';
void main() => foo(); // Prints 'vm foo' with `dart`, 'dart2js foo' with `dart2js`.
lrhn commented 4 years ago

We can probably do Flutter-specific features as:

export "riverpod.dart" 
  if (dart.library.io) "riverpod_flutter.dart";

where riverpod_flutter.dart exports riverpod.dart and adds Flutter-specific operations as well.

I really, really discourage doing it, though. Always try to have a conditional import/export have the same API on all branches. Otherwise the code using the non-general API won't even compile in other configurations (unless it's extra methods on some objects, and you call them dynamically guarded by a bool.fromEnvironment("dart.lanugage.io"), but then you can do the same without exposing those methods in the API at all, just put them in the implementation classes).

If someone needs riverpod_flutter.dart, they should import that directly, because then it's obvious that the code is Flutter-specific. Otherwise they might develop the code, test it on Flutter and succeed, then have other people fail to compile on non-Flutter.

So, if I'm understanding the feature, it's more like:

import "riverpod.dart" with flutter, flutter_hooks;

where the same library URI can be configured in multiple ways, but without requiring an exponential explosion in possible import files, and without requiring separate imports for each feature. A configurable meta-import.

It still resolves to a single API.

Strawman syntax:

// Import
import "lib.dart" with feature1, feature2; // (deferred? as x)?  (show/hide ...)  afterwards, conditional import before.

and

// export
export "feature1.dart" as feature1; // can be `export "feature1.dart" with subfeature1, subfeature2 as feature1;` as well.
export "feature2.dart" as feature2;

Basically, it's a parameterized library, where the features are parameters, combined with conditional exports guarded by those parameters. Since the parameters must be provided in source code, it's possible to statically analyze the consequences. We'd need some kind of conflict resolution. Perhaps analyzing the exporting library itself should check that the exports generate no conflict, even if they are all exported.

I don't think I'd want the features to be declared in-line in the same library. A feature should be an independent library, because that allows it independent imports, and we can then determine which features are platform specific. Say:

library something.general;
export "io_features" as io;  // Depends on `dart:io`.
...

Then a library which doesn't mind depending on dart:io can do:

import "something.dart" with io;

instead of doing

import "something.dart";
import "something_io.dart";
// or 
import "something_io.dart"; // exports something.dart

and the latter scales to, say, four features without requiring sixteen different libraries for all the combinations.

TimWhiting commented 4 years ago

@rrousselGit Yes, this issue is to address use cases like yours.

@eernstg The issue with conditional exports as it stands is that you only can do them based on dart.* libraries right? So that works if you want different exports with the same api depending on io / web. But it doesn't scale to the case where you want to add additional features to a library if the user is using the flutter sdk, or angular_dart, etc.

@lrhn Sorry, I didn't get back to you earlier. I've been busy with other things. I think you captured most of it in your last comment & I like the syntax you proposed.

I agree that these parameterized exports (features) should be independent libraries.

The one thing that this doesn't address, is the following situation, I'll put it in the context of riverpod since that was brought up. Library creator:

// riverpod.dart
library riverpod;
export 'riverpod_common.dart';
export 'riverpod_flutter.dart' as flutter;
export 'riverpod_angular.dart' as angular;

Consumer: Wants to use the angular_dart feature, and doesn't have flutter installed.

import 'riverpod.dart' with angular;

Problem: This will cause problems when running pub get because the consumer doesn't have the flutter sdk, but is using a library that probably depends on the sdk. It also means that running pub get would potentially pull down many more packages than you are actually using.

Potential Solution: Statically it is known which dependencies in the consumed package are actually used, because in the common and angular libraries there will be no imports of flutter. Essentially solving this problem would require tree-shaking of dependencies in the pubspec, rather than only tree shaking dead code.

Caveats and Alternative: The previous solution using static analysis, means that in order to run pub get you have to statically analyze the code, which I imagine would be a no-go? So instead the proposal would be to require the author of the package to explicitly state which packages / sdks are additional requirements for certain features in the pubspec, and throw an error if the library author imports a package that is not available for that feature. However, this also might create problems for the consumer where they don't have one of these 'prerequisite' packages downloaded. So this means either pub get must still analyze the users' code to see what features they are using, or they must explicitly state in the pubspec which features of a particular package they are using, before they can use them in their code. So essentially this introduces a static analysis step to pub get, or requires explicit feature dependencies & some changes to the pubspec format. I think most users/library authors might prefer the more explicit solution. Another benefit of this more explicit dependency formalization for features, is that potentially a feature could add extension methods if the user is using a recent enough dart sdk without breaking the library for those on an older dart sdk.

eernstg commented 4 years ago

@TimWhiting wrote:

The issue with conditional exports as it stands is that you only can do them based on dart.* libraries right?

It's true that the configurable URIs are quite restricted in the set of conditions that they can test for. I just responded to the comment by @rrousselGit where the idea of using configurable exports was mentioned as a possible way ahead.