Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow parameter defaults for null values #1512

Open
lukepighetti opened this issue Jul 19, 2018 · 19 comments
Open

Allow parameter defaults for null values #1512

lukepighetti opened this issue Jul 19, 2018 · 19 comments

Comments

@lukepighetti
Copy link

Please save us from having to do this to capture both undefined and null values. 😃 I know undefined isn't a thing in Dart but it is treating "unspecified" as being different than "null".

void main() {
  Thing nullThing = Thing(text: null);
  print(nullThing.text); // "default"

  Thing undefinedThing = Thing();
  print(undefinedThing.text); // "default"
}


class Thing{
  String text;
  
  Thing({
    String text: "default"
  }) : this.text = text ?? "default";
}

Ideally:

class Thing{
  String text;
  
  Thing({
    this.text: "default"
  });
}

Why? Because when being passed Maps from a NoSQL database that may not have complete data, and then using it to create a new class instance, I have not found a good way to pass undefined to the class constructor arguments, when myMap['notHere'] == null

@lrhn
Copy link
Member

lrhn commented Jul 20, 2018

I agree that it would be ideal if explicitly passing null would be treated the same as not passing the parameter at all (which is not undefined, but I can see the resemblance).
That is, foo({x = "default"}) { ...} should implicitly be the same as foo({x}) { x ??= "default"; ...}.

I hope to be able to make that change some time in the future.
If we introduce non-nullable types, it likely becomes much more important. That would also allow for required named arguments (a named argument with a non-nullable type and no default value).

Currently, I personally prefer to use ?? to initialize optional parameters, and only use default values for parameters which I would consider required (where I don't mind passing null being an error, which is typically boolean named arguments only).\

@lukepighetti
Copy link
Author

lukepighetti commented Jul 20, 2018

I would also like to extend this request to @required in meta

class Project{
  final int id;

  Project({
    @required this.id,
  }) : assert(id != null);
}

could be

class Project{
  final int id;

  Project({
    @required this.id,
  });
}

@jarrodcolburn
Copy link

jarrodcolburn commented Jan 9, 2019

I wanted to provide a different example where providing null to parameters results in suppression of the default values.

Example references the FloatingActionButton FAB class constructor from Flutter package:

// from Flutter 
class FloatingActionButton extends StatefulWidget {
  const FloatingActionButton({
      Key key,
      this.child,
      this.tooltip,
      this.foregroundColor,
      this.backgroundColor,
      this.heroTag = const _DefaultHeroTag(),
      this.elevation = 6.0,
      this.highlightElevation = 12.0,
      @required this.onPressed,
      this.mini = false,
      this.shape = const CircleBorder(),
      this.clipBehavior = Clip.none,
      this.materialTapTargetSize,
      this.isExtended = false,
    }) :  assert(elevation != null && elevation >= 0.0),
        assert(highlightElevation != null && highlightElevation >= 0.0),
        assert(mini != null),
        assert(shape != null),
        assert(isExtended != null),
        _sizeConstraints = mini ? _kMiniSizeConstraints : _kSizeConstraints,
        super(key: key);
// remaining class definition...
}

The code above is imported and cannot be modified by end user.

The code below is a user trying to create a class that builds a FAB. Each argument of FAB constructor is constructed by a method. Each method can return a value or null (null would ideally then defers to FABs defaults).

class MyClass /* extends StatelessWidget */ {

  bool getMini(){ /* may return a `bool` or `null` */ }
  bool getExtended(){ /* may return a `bool` or `null */` }
  double getElevation(){ /* may return a `double` or `null`*/ }
  double getHighlightElevation(){ /* may return a `double` or `null`*/ }
  Hero getHero(){ /* may return new `Hero` or `null`*/ }
  Shape getShape(){ /* may return new `Shape` or `null`*/ }
  Clip getClipBehavior(){ /* may return new `Clip` or `null`*/ }

  build(_){
    return FloatingActionButton(
      mini = getMini(),
      isExtended = getExtended(),
      elevation = getElevation(),
      highlightElevation = getHighlightElevation(),
      heroTag = getHero(),
      shape = getShape(),
      clipBehavior = getClipBehavior(),
      // remaining FAB arguments (child, onPressed...)
    );
  }
}

Comaring expected vs actual results

Desired/expected results:

  • If null is passed as argument to constructor, it ignores null for default value
    • example: if getMini() returns null, passes it to the mini argument of FloatingActionButton constructor, the null should be ignored and it's own internal default true should be used.

Actual results:

  • If null is passed as an argument, defaults values are ignored.
    • example: if getMini() returns null, passed to mini argument of FloatingActionButton constructor, mini variable remains null, ignoring default value and an error will be thrown.

Because null can't be passed to constructors, to get desired results one constructor must potentially be created for every iteration.

  • Because the above code doesn't work as expected, it must be rewritten a different way. For simple situations, a ternary can be used for multiple constructors, but quickly gets unwieldy.

    • The above code example, since none of the args are mutually exclusive and all variables are final, must be known at the time of constructor, ...all situations must be written. The 7 arguments would require writing ~127 versions of constructors!
    • Here for only the top 3 (of the 7) variables (mini, isExtended, elevation) are the 8 constructors that would be required to implement code: (code snippet is argument assignment)
      build(_){
      return FloatingActionButton(
      • none (of first three) with value
        // mini = null,
        // isExtended = null
        // elevation = null 
      • first with value
        mini = getMini(),
        // isExtended = null
        // elevation = null 
      • second with value
        // mini = null,
        isExtended = getExtended(),
        // elevation = null 
      • first two with value
        mini = getMini(),
        isExtended = getExtended(),
        // elevation = null 
      • third with value
        // mini = null,
        // isExtended = null
        elevation = getElevation(),
      • first and third with value
        mini = getMini(),
        // isExtended = null
        elevation = getElevation(),
      • second and third with value
        // isExtended = null
        isExtended = getExtended(),
        elevation = getElevation(),
      • all three with value
        mini = getMini(),
        isExtended = getExtended(),
        elevation = getElevation(),
          // heroTag, shape, clipBehavior...
          // remaining FAB arguments (child, onPressed...)
        );
      }
  • Although changes to the constructor would allow it to work since it's imported the end user cannot modify:

    • Changing the constructor signature from FloatingActionButton({this.mini = false}); to FloatingActionButton({bool mini}): thi.mini = mini ?? true; can't be made as in another package.
  • The end user should be able to rely on the package's defaults, not having to implement his own or duplicate the package`s default which would be redundant code.

    • Although the end user can see, from Flutter source code, that package default for mini is false and a working implementation would be:
    return FloatingActionButton(
      mini = getMini() ?? false,
      //...
    )

    this would require all the defaults needing to be re-implemented by end user, resulting in duplication and possible difficult to detect bugs if the package is changed in future.

note: Although using a Flutter class within example, the issue is not specific to Flutter.

@MeamonB
Copy link

MeamonB commented Jan 21, 2019

I'm new to dart and this behavior was a big stumbling block for me, the error I was getting didn't suggest that null value was doing anything. It would be a huge benefit for me and many other beginners for this to be resolved!

@lukepighetti
Copy link
Author

@MeamonB I think this will become more clear if and when Dart gets non nullable types. I am personally waiting to see if that happens before I worry about this issue more. Just my opinion.

@Nico04
Copy link

Nico04 commented Sep 4, 2019

Same issue here, I've spend many hour trying why the default value didn't get set correctly until I realise if we pass null as a parameters it doesn't get his default value... Very annoying !

@Reprevise
Copy link

This would be very helpful when developing packages that interface with other packages and many other scenarios too but I'm just frustrated because I'm trying to make a package and I would really like this to be added.

@Skyost
Copy link

Skyost commented Jan 2, 2020

I agree with everyone here. Here's a little proposal; keep the current system but add an operator like this : ??= which would work on undefined and on null values.

class Person {
  String firstName;
  String lastName;

  Person({this.firstName ??= 'John', this.lastname ??= 'Watson'}); 
}

Would be the same as :

class Person {
  String firstName;
  String lastName;

  Person({
    String firstName,
    String lastName,
  })  : this.firstName = firstName ?? 'John',
        this.lastName = lastName ?? 'Watson';
}

@omneimneh
Copy link

omneimneh commented Apr 20, 2020

Here's another proposal, just like dart optional values in array:

SnackBar(
      content: Text(message),
      duration: if (retryAction != null) Duration(days: 365),
)

This would really boost productivity instead of having to write the initialization twice

@Nico04
Copy link

Nico04 commented Apr 20, 2020

SnackBar(
      content: Text(message),
      duration: if (retryAction != null) Duration(days: 365),
)

Or like in lists or maps :

SnackBar(
      content: Text(message),
      if (retryAction != null) 
           duration: Duration(days: 365),
)

@lrhn
Copy link
Member

lrhn commented Apr 20, 2020

Using if for optional named parameters is definitely possible. My main worry about it is that it doesn't work for optional positional parameters, at least not the way they are currently defined.
That asymmetry would encourage using named parameters over positional parameters even in situations where positional parameters would otherwise be the better design choice.

If we also allow you to omit no-trailing optional positional parameters, so you an write foo(1,,3) to call int foo(int x, [int y = 42, int z]) and get the default value for y, then we can also allow writing foo(1, if (test) 2, 3) to conditionally omit the parameter.

@lrhn lrhn closed this as completed Apr 20, 2020
@lrhn lrhn reopened this Apr 20, 2020
@benzsuankularb
Copy link

I think this may be fixed after Dart introduced non-nullable types?

@spragucm
Copy link

spragucm commented Aug 23, 2020

This is sooooo hacky and makes me feel like I'm missing something critical, but it works.

ThemedSwitch({this.key,
    @required value,
    @required this.onChanged,
    this.activeColor,
    this.activeTrackColor,
    this.inactiveThumbColor,
    this.inactiveTrackColor,
    this.activeThumbImage,
    this.onActiveThumbImageError,
    this.inactiveThumbImage,
    this.onInactiveThumbImageError,
    this.materialTapTargetSize,
    this.dragStartBehavior,
    this.mouseCursor,
    this.focusColor,
    this.hoverColor,
    this.focusNode,
    this.autofocus}) {

    //Construct a default widget in order to fall back on default values when the optional params aren't passed
    var defaultSwitch = Switch(value: value, onChanged: onChanged);

    _switch = Switch(
        value: value,
        onChanged: onChanged,
        activeColor: activeColor != null ? activeColor : defaultSwitch.activeColor,
    );
  }

Basically, to override the super's functionality without passing on the nulled optional arguments (ie the optional args that were not passed into the constructor), create one widget with the required fields and then use a conditional argument that falls back on the default values from that widget (see activeColor above)

Dart...please make it easier to extend the constructor!

@enkr1
Copy link

enkr1 commented Jan 20, 2021

Hi, how does ??= work and how do we call it?
thanks in advance

@rockingdice
Copy link

This is sooooo hacky and makes me feel like I'm missing something critical, but it works.

ThemedSwitch({this.key,
    @required value,
    @required this.onChanged,
    this.activeColor,
    this.activeTrackColor,
    this.inactiveThumbColor,
    this.inactiveTrackColor,
    this.activeThumbImage,
    this.onActiveThumbImageError,
    this.inactiveThumbImage,
    this.onInactiveThumbImageError,
    this.materialTapTargetSize,
    this.dragStartBehavior,
    this.mouseCursor,
    this.focusColor,
    this.hoverColor,
    this.focusNode,
    this.autofocus}) {

    //Construct a default widget in order to fall back on default values when the optional params aren't passed
    var defaultSwitch = Switch(value: value, onChanged: onChanged);

    _switch = Switch(
        value: value,
        onChanged: onChanged,
        activeColor: activeColor != null ? activeColor : defaultSwitch.activeColor,
    );
  }

Basically, to override the super's functionality without passing on the nulled optional arguments (ie the optional args that were not passed into the constructor), create one widget with the required fields and then use a conditional argument that falls back on the default values from that widget (see activeColor above)

Dart...please make it easier to extend the constructor!

Met exactly the same problem.
I think this issue could be related to #216
If the constructor can be used as a function, then you could do this:

Container container = Function.apply(Container.ctor, [], {Symbol("alignment") : Alignment.bottomCenter});

And just ignore those arguments from the named argument list that need to be using the initial value.

@rockingdice
Copy link

This is sooooo hacky and makes me feel like I'm missing something critical, but it works.

ThemedSwitch({this.key,
    @required value,
    @required this.onChanged,
    this.activeColor,
    this.activeTrackColor,
    this.inactiveThumbColor,
    this.inactiveTrackColor,
    this.activeThumbImage,
    this.onActiveThumbImageError,
    this.inactiveThumbImage,
    this.onInactiveThumbImageError,
    this.materialTapTargetSize,
    this.dragStartBehavior,
    this.mouseCursor,
    this.focusColor,
    this.hoverColor,
    this.focusNode,
    this.autofocus}) {

    //Construct a default widget in order to fall back on default values when the optional params aren't passed
    var defaultSwitch = Switch(value: value, onChanged: onChanged);

    _switch = Switch(
        value: value,
        onChanged: onChanged,
        activeColor: activeColor != null ? activeColor : defaultSwitch.activeColor,
    );
  }

Basically, to override the super's functionality without passing on the nulled optional arguments (ie the optional args that were not passed into the constructor), create one widget with the required fields and then use a conditional argument that falls back on the default values from that widget (see activeColor above)
Dart...please make it easier to extend the constructor!

Met exactly the same problem.
I think this issue could be related to dart-lang/language#216
If the constructor can be used as a function, then you could do this:

Container container = Function.apply(Container.ctor, [], {Symbol("alignment") : Alignment.bottomCenter});

And just ignore those arguments from the named argument list that need to be using the initial value.

From:
#216 (comment)

Function.apply may have a critical performance issue. So it's better to use another way to deal with the default values issue.

I think it's good to have a unspecified symbol for the usage of the argument.

@mit-mit mit-mit transferred this issue from dart-lang/sdk Mar 11, 2021
@rockingdice
Copy link

This is sooooo hacky and makes me feel like I'm missing something critical, but it works.

ThemedSwitch({this.key,
    @required value,
    @required this.onChanged,
    this.activeColor,
    this.activeTrackColor,
    this.inactiveThumbColor,
    this.inactiveTrackColor,
    this.activeThumbImage,
    this.onActiveThumbImageError,
    this.inactiveThumbImage,
    this.onInactiveThumbImageError,
    this.materialTapTargetSize,
    this.dragStartBehavior,
    this.mouseCursor,
    this.focusColor,
    this.hoverColor,
    this.focusNode,
    this.autofocus}) {

    //Construct a default widget in order to fall back on default values when the optional params aren't passed
    var defaultSwitch = Switch(value: value, onChanged: onChanged);

    _switch = Switch(
        value: value,
        onChanged: onChanged,
        activeColor: activeColor != null ? activeColor : defaultSwitch.activeColor,
    );
  }

Basically, to override the super's functionality without passing on the nulled optional arguments (ie the optional args that were not passed into the constructor), create one widget with the required fields and then use a conditional argument that falls back on the default values from that widget (see activeColor above)
Dart...please make it easier to extend the constructor!

Met exactly the same problem.
I think this issue could be related to dart-lang/language#216
If the constructor can be used as a function, then you could do this:

Container container = Function.apply(Container.ctor, [], {Symbol("alignment") : Alignment.bottomCenter});

And just ignore those arguments from the named argument list that need to be using the initial value.

From:
#216 (comment)

Function.apply may have a critical performance issue. So it's better to use another way to deal with the default values issue.

I think it's good to have a unspecified symbol for the usage of the argument.

Just a confirmation - The Function.apply did bad performance after tests.
I don't know if it's something related to the default values.

@jamesderlin
Copy link

I personally like the suggestion (from #1639 (comment), although I think it's been suggested in other places) of reusing the existing default keyword to explicitly use the callee's default argument for the corresponding parameter.

@eernstg
Copy link
Member

eernstg commented Dec 11, 2022

See also #2269.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests