language icon indicating copy to clipboard operation
language copied to clipboard

Add a "namespace" keyword

Open nate-thegrate opened this issue 2 years ago • 10 comments

The Problem

There are many places in the Flutter repository where a class is used as a namespace, often to store many const values of the same type. I've found examples of this in autofill.dart, colors.dart, curves.dart, icons.dart, and unicode.dart.

And there are many other places in the Flutter repo where a class is used to hold static members that aren't just const values, which directly goes against Dart's style guide. I found 7 files in the services/ folder with a class that's structured in this way (clipboard.dart, deferred_component.dart, haptic_feedback.dart, system_channels.dart, system_chrome.dart, system_navigator.dart, and system_sound.dart).

Despite how it's apparently a bad coding practice, using a class as a namespace seems to provide a lot of utility within the Flutter repository, and I'm sure many others (myself included) are creating classes as namespaces in their own projects.


Workarounds

Let's use a simplified version of Colors as an example:

class Colors {
  // This class is not meant to be instantiated or extended; this constructor
  // prevents instantiation and extension.
  Colors._();

  static const Color red = Color(0xFFFF0000);
  static const Color blue = Color(0xFF0000FF);
}

There are some great ways to store a bunch of const values of the same type, including List and Map:

const List<Color> colorList = [Color(0xFFFF0000), Color(0xFF0000FF)];

const Map<String, Color> colorMap = {
  "red": Color(0xFFFF0000),
  "blue": Color(0xFF0000FF),
};

Sadly, these options can't match the autofill convenience that classes provide:

colors autofill


Dart 2.17 allows enums to contain other types:

enum Colors {
  red(Color(0xFFFF0000)),
  blue(Color(0xFF0000FF));

  final Color color;
  const Colors(this.color);
}

But why would you want to type out Colors.red.color every time you need the color red when you can just use Colors.red by making a class instead?


The Dart style guide recommends implementing a namespace as a library:

import "./colors.dart" as colors;

But this would require you to devote an entire .dart file to a single namespace, which can be inconvenient in many situations.


Fortunately, I learned from @lrhn that you can create an extension on the Never class as a makeshift namespace:

// extension on Never acts as a namespace
extension Colors on Never {
  static const Color red = Color(0xFFFF0000);
  static const Color blue = Color(0xFF0000FF);
}

This syntax, while definitely not intuitive, is better than class Colors with a Colors._() constructor since it can't be implemented, making it functionally identical to a namespace. There's currently a pull request open on the Flutter repo to enact this change, so perhaps this is what namespaces like Colors will look like in the future.


And if you're hoping for a syntax that's intuitive enough that you don't need a comment explaining what it does, you can use the abstract keyword:

abstract class Colors {
  static const Color red = Color(0xFFFF0000);
  static const Color blue = Color(0xFF0000FF);
}

This doesn't prevent you from extending the class, implementing it, or using it as a mixin, but a Dart developer who knows how to use extends, implements, and with will understand that an abstract class with all static members is meant to be used as a namespace.


Proposal

It looks to me like we have two options moving forward.


1. Don't change the Dart language

If we don't end up implementing any changes to the Dart syntax, we still should update the Dart style guide with information about using an extension on Never (assuming we decide to go through with the aforementioned pull request).

Alternatively, we could update our documentation to recommend using an abstract class with static members in place of a namespace, since it's much more intuitive than extension on Never.


2. Add a namespace keyword

class Colors {
  // This class is not meant to be instantiated or extended; this constructor
  // prevents instantiation and extension.
  Colors._();

  static const Color red = Color(0xFFFF0000);
  static const Color blue = Color(0xFF0000FF);
}

could instead be constructed as

namespace Colors {
  const Color red = Color(0xFFFF0000);
  const Color blue = Color(0xFF0000FF);
}

We could even have keywords that cover common use cases:

const namespace Colors {
  Color red = Color(0xFFFF0000);
  Color blue = Color(0xFF0000FF);
}

or maybe even

const Color namespace Colors {
  red = Color(0xFFFF0000);
  blue = Color(0xFF0000FF);
}

Even just a quick and dirty keyword addition (i.e. namespace [Name] = extension [Name] on Never with all static members) would be beneficial to people who use or contribute to the Flutter codebase, which is nearly everyone who uses Dart.

nate-thegrate avatar Jun 03 '22 04:06 nate-thegrate

For the enum option, 2.17 also allow enums to extend other classes:

enum Colors extends Color {
  red(0xFFFF0000),
  blue(0xFF0000FF),
  // ...
  purple(0xFFFF00FF),
  ;

  const Colors(super.rgba);
}

No need to have an extra .color. It does mean that the runtimeType of the values are Colors instead of Color, and that may cause some polymorphism, but they do implement Color.

I'd usually put the constants of a type into that type itself, so it would be Color.purple instead of Colors.purple. I guess that's impossible for Colors if Color has a red getter too. That also prevents the enum Colors extends Color approach.

Personally, I'd just use abstract class Colors { ... } and not bother with the constructor trying to prevent people from doing something that has no benefit to them anyway. They can still implement Colors, but nobody ever does, because it has no value. Preventing extension and instantiation is only valuable in that it makes changing to a real namespace theoretically non-breaking, rather than just practically non-breaking - which it already is.

lrhn avatar Jun 03 '22 10:06 lrhn

@lrhn I tried copy-pasting your snippet, and it gave me an error:

enum extends

I saw in the Dart language tour that enum already extends the Enum class, so it can't extend anything else.


Personally, I'd just use abstract class Colors { ... }

This is a great idea, at least for anyone who's going for the best combination of readability and functionality that we have right now. The abstract keyword immediately tells you "this class can't be instantiated", and anyone who has experience with the language will see all the static members and understand what to use it for.

I've updated my original issue comment to include this suggestion.

nate-thegrate avatar Jun 03 '22 16:06 nate-thegrate

Damnit, my bad. You can implement an interface, and apply a mixin, but an enum can't extend another class than Enum.

lrhn avatar Jun 03 '22 19:06 lrhn

If we did have a namespace ... { ... } declaration, we'd have to decide on a naming style.

We usually use UpperCamelCase for types, and it's probably just consistency that made us do the same for extension — which does not introduce a type, and which is rarely written by name at all, but since it's rarely written, it doesn't matter.

We do have a tradition for namespaces. The one namespace we already have is the import prefix, which uses snake_case. Library names too, which were originally thought of as a kind of namespace identifier.

That makes it likely that we'll recommend namespace colors { ... } rather than namespace Colors { ... }, which will make migration hard (or the result inconsistent).

We could introduce namespace aliases: namespace Colors = colors; We could even introduce modifiers like namespace Colors = colors hide NewBlack;.

lrhn avatar Jun 04 '22 07:06 lrhn

@lrhn I'm glad you brought this up.

I personally don't have a strong preference about snake_case vs. UpperCamelCase. It's probably best to follow the import prefix convention, but I also see some similarity between namespace and enum.
They're both declared with a name and curly braces (i.e. namespace Colors { ... } and enum Colors { ... }), and if we eventually change enums so that you could declare enum Colors { ... } in a way that Colors.red had the type Color, it might make sense for namespaces to use UpperCamelCase to allow easy switching between the two.

Namespace aliases would be really nice to have; adding modifiers like hide would also be great and would conceptually push namespaces more in the direction of import prefixes.

We'd just have to decide which precedent makes more sense:

  • snake_case for all namespaces, including everything introduced via import and namespace keywords
  • UpperCamelCase for class, enum, extension, etc.

or

  • snake_case for everything related to libraries/packages/file names (e.g. import 'file_name.dart' as file_name)
  • UpperCamelCase for everything within a file, including class, enum, and namespace

If namespaces do get implemented at some point, I'd be happy with either of these.

nate-thegrate avatar Jun 04 '22 19:06 nate-thegrate

const namespace Colors as Color {
  red = Color(0xFFFF0000);
  blue = Color(0xFF0000FF);
}

I'd prefer a syntax like this although it looks like a variable declaration.

const Color namespace Colors {
  red = Color(0xFFFF0000);
  blue = Color(0xFF0000FF);
}

or

namespace Colors as const Color {
  red = Color(0xFFFF0000);
  blue = Color(0xFF0000FF);
}

Having const Color together makes more sens to me for some reason. Wouldn't that be close to a const named tuple with a typedef on top though ?

cedvdb avatar Jun 05 '22 22:06 cedvdb

It seems to me that namespaces could make (shorter dot syntax) #357 more general, and also maybe address (static extension methods) #723.

namespace Colors on Color {
// Static keyword is implicit in all these declarations, as if these were top-level declarations,
// but they are actually registered in the static namespace of Colors 
// as well as the Color class (when there is no conflicts)
  const red = Color(0xFFFF0000);
  const blue = Color(0xFF0000FF);
  const myAwesomeColor = Color(0xFF0F0FFF);
  Color get randomColor => Random()..nextInt(0xFFFFFFFF);
  int _nextColor = 0x00000000;
  Color nextColor() {
     return Color(_nextColor++);
  }
}

The namespace makes these members accessible by the prefix Colors.. However the on Color (optional for basic namespaces) makes red and blue usable as if they were static members on Color with the caveat that if red and blue are already introduced into the namespace of Color then you must use the Colors. prefix to disambiguate just like extension methods. Static members on a class are basically just namespaced top-level members anyways since there is no inheritance for them. This also handles registering these constants as being a part of the namespace of Color so that shorter dot syntax #357 can be enabled for these constants as well. In general if the context type is Color all of the statics in the Color namespace that return a subtype of Color are enabled for shorter dot syntax.

return ColoredBox(color: .randomColor, child: Text('Something'));
return ColoredBox(color: .nextColor(), child: Text('Something'));
return ColoredBox(color: .myAwesomeColor, child: Text('Something'));
return ColoredBox(color: .red, child: Text('Something'));

Edit: I realize this is just sugar for extension method syntax except with the static members from the extension method namespace being copied to the 'on' class when there are no conflicts, or being an extension on 'Never' if there is no 'on' class. Is there a way to enable that for extension methods in the first place? It might be nice to have the 'namespace' keyword to better relate what the purpose is for static members, but at that point it could just be syntax sugar.

TimWhiting avatar Jun 06 '22 02:06 TimWhiting

is namespace is the same as abstract closed base class in https://github.com/dart-lang/language/issues/2242 ?

Hixie avatar Jun 28 '22 20:06 Hixie

@Hixie I believe so, although namespace would implicitly give all members the static property as well.

nate-thegrate avatar Jun 28 '22 21:06 nate-thegrate

It would be nice to have an enum like a abstract class but with heavy restrictions like accepting only static const types with implicit cast.

enum Colors
{
  blue = Color(0xFF0000FF),
  red = Color(0xFFFF0000);
}

Color a = Colors.red;

The idea is to prefer enum for constants without having to use a more general purpose notation like abstracts or a library.

I know, depending on the declaration the call return type would change...

tekert avatar Aug 26 '22 02:08 tekert

Thanks to all the class modifiers introduced in Dart 3.0, I think we're good to close this issue.

There wasn't anything that does exactly what I suggested, but we have several good options now—abstract final class does a pretty great job.

nate-thegrate avatar Sep 06 '23 20:09 nate-thegrate