dart-lang / language

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

Syntactic sugar for `typedef` #4112

Open FMorschel opened 1 week ago

FMorschel commented 1 week ago

Today if I have two libraries that export classes with the same name, we get ambiguous_import if we try to use it. The suggested workarounds are to add an alias to one (or both) imports or to use hide.

Most of the time this is just fine, I'd like to propose a syntactic sugar to make the class names clearer on use.

Today we can do the following:

a.dart

class Random {}

// Other things that our code might use
class A {}

main.dart

import 'dart:math';
import 'a.dart' as a show Random;
import 'a.dart' hide Random;

typedef MyRandom = a.Random;

A? aInstance;
MyRandom? myRandom;
Random? random;

I'd like to propose a syntactic sugar for imported elements:

import 'dart:math';
import 'a.dart' with Random as MyRandom;

I'm not sure what word to use on with, just a suggestion. Using a different word to mean that I don't want to show or hide anything, but I believe this should work on show as well.

lrhn commented 6 days ago

This is a renaming operator, not just a type alias. Nothing would prevent it from renaming a variable or function.

It's not a new idea, it has been known since the hide and show modifiers were added, but it's not clear that a local rename operation is worth the complexity, when you can use an import prefix to avoid name conflicts. Allowing renames in exports might be a little too confusing.

FMorschel commented 6 days ago

Allowing renames in exports might be a little too confusing.

I agree, but today this can be done anyway with typedef so I don't see why not.

lrhn commented 6 days ago

My reason against that would be that a rename doesn't expose a declaration.

If you expose a name using a typedef, there is an actual declaration with that name that one can look at and document. If you do export 'dart:core' show String as Text; and I look at Text and go to source, I'll be confused if I find class String in dart:core. If it goes to String as Text, I might understand what's going on, but I still don't know why, and there is no way to add documentation to it.

With

import 'dart:core' as core;
/// A text used by something or other.
typedef Text = core.String;

you get to say what the new name means.

The core question is which problem this feature solves.

Is it having to write a typedef to rename something inside the same library or package, just for convenience? In that case, I don't think the benefit is big enough to warrant a language feature. Just write the typedef once and for all, and import it where you need it.

To not have to do the three-step-name-bypass, two imports, one without prefix and with a hide, one with prefix, and then one typedef to give a new unprefixed name for the name that would conflict? I still think it's rare enough, and local enough, that it doesn't really need a language feature. I'd actually prefer to use the prefixed name just to make it clear what is going on, rather than have an alias for it. (And if it's intended to be local only, the new should start with an _, so it won't be much prettier than a.Name.)

Is it intended as a way to expose an existing type by another name in a public API? Then I think you should document it, so using a typedef is better.

Is it exposting the same declaration under different names (not just the same type, like a type alias)? A type alias has covered that pretty well since it started allowing static member accesses. I'm not seeing the problem that solves.

Just to be realistic, language features are not free. They need to carry their own weight, with the benefit they add outweiging the cost of adding them, including opportunity cost. This feature, even if it looks fairly simple, doesn't seem likely to be worth its own design and implementation. The problems it addresses don't happen often enough and the workaround isn't problematic enough, so I expect the net benefit to be low. If people would regularly make mistakes in the workaround, then saving them from a pitfall might increase the value, but this would only save typing. There are other places where one can save typing, more commonly hit, that I'd go for first.

Not saying it wouldn't be nice to have.

Wdestroier commented 6 days ago

The core question is which problem this feature solves.

I will explain why I gave this issue a thumbs up. I was creating a card game and I want to call "Card" one of the most important project classes. However, it's a Flutter project and Flutter has a class called "Card". Therefore I need to write import 'package:flutter/material.dart' hide Card;' in almost every file. Would be nice if I could alias the Card widget as FlutterCard, MaterialCard or CardWidget, because then I can use the widget somewhere. The problem this feature doesn't solve is that I still need to fix the import in other files.

mateusfccp commented 6 days ago

The core question is which problem this feature solves.

I will explain why I gave this issue a thumbs up. I was creating a card game and I want to call "Card" one of the most important project classes. However, it's a Flutter project and Flutter has a class called "Card". Therefore I need to write import 'package:flutter/material.dart' hide Card;' in almost every file. Would be nice if I could alias the Card widget as FlutterCard, MaterialCard or CardWidget, because then I can use the widget somewhere. The problem this feature doesn't solve is that I still need to fix the import in other files.

In my opinion, if you are making a card came, you shouldn't be depending on material anyway.

Not saying that I disagree (or agree) with this issue.

lrhn commented 6 days ago

One thing you can do is have your own library:

// lib/src/nocard_material.dart
import 'package:flutter/material.dart' as card;
export 'package:flutter/material.dart' hide Card;
typedef FlutterCard = card.Card;

and then import that everywhere you would otherwise import package:flutter/material.dart.

That can even be easier to use than having to do import 'package:flutter/material.dart' with Card as FlutterCard; in every library you import it in. Doing it once and for all, in your own library. And you can give it a shorter name if you prefer, so you can just write import '/src/ncm.dart';. Less typing, and a single place to make changes in the future, if you need more.

Wdestroier commented 6 days ago

You can have your own library and then import that everywhere.

I may have a library exporting flutter then, thanks Lasse!

In my opinion, if you are making a card came, you shouldn't be depending on material anyway.

Possibly, I can try to import widgets.dart instead of material.dart.

TekExplorer commented 6 days ago

and then import that everywhere you would otherwise import package:flutter/material.dart.

I think we need improved tooling for this.

When I once attempted something similar, the inconvenience of needing to select the correct import - and needing to remember it - is not great.

Perhaps we should have some kind of annotation we could use to say that "this export is preferred over importing directly"? Or perhaps that should be its own issue? I'm not sure where it would go though.