Language: [Feature] Add support for an Undefined data type

Created on 7 Mar 2020  路  11Comments  路  Source: dart-lang/language

Since we have this type in other languages, it would be nice to have it in Dart. It'll greatly strengthen the usefulness of copyWith functions, so that null values can be differentiated from values that have not been defined.

Use case:

class MapState1 {
  MapState copyWith({
    Category activeTopCategory,
    Category activeCategory,

  }) {
    return MapState(
      activeTopCategory: activeTopCategory ?? this.activeTopCategory,
      activeCategory: activeCategory ?? this.activeCategory,
    );
  }

If I decide to set the activeCategory to null, the newly created object will have the same values as the old object. I cannot figure out a reasonable workaround for this.

The second use-case is more common in widgets with optional named parameters with default values. If someone passes in that value using a variable and that variable turns out to be null, we don't get the default value, and there's no way to pass in a value sometimes and leave it undefined at others without creating 2 separate widgets.

feature

Most helpful comment

IMO this would be properly fixed by union types

This would allow people to make their own Undefined if they need it:

class _Undefined {
  const _Undefined();
}

SomeClass copyWith({
  int  | _Undefined someProperty = const _Undefined(),
}) {
  return SomeClass(someProperty: someProperty is int ? someProperty : this.someProperty);
}

All 11 comments

The second use-case emerges also when I try to extend a constructor with optional default values. I haven't figure out how to form the super invocation in order to not overlap the base class default values.

While I have had the same issue, I'm not sure adding a new type is a good solution in a statically typed language like Dart.

If we add an Undefined type and a corresponding undefined value, we need to figure out where those fit into the current type system, and into the next type system where not all types are nullable.
I just don't see any good solution to that. We'd have all the same complexities as for Null in the Null Sound type system, just multiplied by 2. We'd need a way to express that the parameter can be Category or Undefined.

I see much more promise in other approaches. For example:

  • Allow non-constant default values.
    Then it would be
    dart MapState copyWith({ Category activeTopCategory = this.activeTopCategory, Category activeCategory = this.activeCategory}) => MapState(activeTopCategory: activeTopCategory, activeCategory: activeCategory);
    so an omitted parameter would get the current value, but an explicit null would not.
    (Although that would preclude my wish of making an explicit null mean the same as omitting
    the parameter).

  • Just using a hidden private default-value:
    dart static const _default = const _CategoryMarker(); // Some special class implementing Category MapState copyWith({ Category activeTopCategory = _default, Category activeCategory = _default}) { if (identical(activeTopCategory, _default)) activeTopCategory = this.activeTopCategory; if (identical(activeCategory, _default)) activeCategory = this.activeCategory; return MapState(activeTopCategory: activeTopCategory, activeCategory: activeCategory); }
    which you can do already today.

IMO this would be properly fixed by union types

This would allow people to make their own Undefined if they need it:

class _Undefined {
  const _Undefined();
}

SomeClass copyWith({
  int  | _Undefined someProperty = const _Undefined(),
}) {
  return SomeClass(someProperty: someProperty is int ? someProperty : this.someProperty);
}

General union types would indeed solve this too. Then you can use any class you choose as placeholder. The current Null type's use in nullable types is just a special cased union type. WIth general union types you could create your own Null-like types wherever you need them.

If we use Union types, won't this have to be explicitly created for each case that I want to check? Seems like a lot of boilerplate

Well, if we truly want to talk about boilerplate, we could talk about data classes or spread on classes, to make a copyWith built in the language.

In any case, union types are much more flexible. They don't benefit almost exclusively a copyWith method.

And we can simplify it a bit:

// Part that can be extracted into a separate file
class Undefined {
  const Undefined();
}

const undefined = Undefined();

T _fallback<T>(T | Undefined value, T fallback) => value is T ? value : fallback;

 // Actual usage

SomeClass copyWith({
  int  | Undefined someProperty = undefined,
  String  | Undefined another = undefined,
}) {
  return SomeClass(
    someProperty: _fallback(someProperty, this.someProperty),
    another: _fallback(another, this.another),
  );
}

This looks great and looks like what I would expect. I'm just used to seeing it work like this in typescript without having to manually define the undefined class.

A better name would be uninitialized

Please, don't. This will only screw with the semantics of the type system. Null is already bad enough and it's giving a lot of trouble to fix it.

or only the dart would indicate if the variable was uninitialized

int n;
int m= null;
int p= 2;

print(n); // output: null (uninitialized)
print(m); // output: null
print(o); // output: 2

would continue to be null, there is no need to create a separate type, dart only needs to indicate whether the variable has been initialized or not

@cindRoberta, you will get a lot of support for tracking the initialization state of variables with the upcoming null-safety feature.

If you just declare variables as usual (without putting a ? on the type), e.g., int i;, then a flow analysis will be used to check that the variable is initialized before it is used. For instance:

void main() {
  int i;
  i = 42; // If you comment this out there is an error in the next line.
  if (i > 0) print(i);
}

This is because i can only have a non-null value (because int, with null-safety, is a type that simply doesn't include null), so the language includes mechanisms to ensure that i is initialized before use. If you use int? i then i can be null, and you need to ensure initialization manually.

You can use a dynamic check as well:

void main() {
  late int i;
  bool b = true; // Assume initialization is complex, we don't know the value.
  if (b) i = 42;
  if (i > 0) print(i);
}

In this case you will get a dynamic error (an exception) when i is evaluated, unless it has been initialized.

The story is a lot longer than this, but as you can see there will be support for tracking initialization when null-safety is released.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

eernstg picture eernstg  路  5Comments

har79 picture har79  路  5Comments

stategen picture stategen  路  4Comments

dev-aentgs picture dev-aentgs  路  3Comments

panthe picture panthe  路  4Comments