Skip to content

Provide an expression to maybe provide an optional parameter #1401

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

Open
Wikiwix opened this issue Jan 13, 2021 · 15 comments
Open

Provide an expression to maybe provide an optional parameter #1401

Wikiwix opened this issue Jan 13, 2021 · 15 comments
Labels
request Requests to resolve a particular developer problem

Comments

@Wikiwix
Copy link

Wikiwix commented Jan 13, 2021

As discussed on Discord there is no short solution to apply an optional parameter maybe a value using an expression:

//@dart=2.12
class Sample {
  const Sample({this.a = 42});
  final int a;
}

Sample someFunction() {
  int? value;
  // ...
  // calculations do potentially set value
  // ...

  // **The following line is not valid in Dart >= 2.12**,
  // but the *expected / hoped for* output would be
  // `value != null ? Sample(a: value) : Sample()`
  return Sample(a: value);

  // # Work-arounds I see currently

  return Sample(a: value ?? 42);
  // Means you hardcode a default that might change over time

  return value != null ? Sample(a: value) : Sample();
  // In this sample okayish, but imagine `Sample` was a `Widget`
  // where you set ~10 named parameters, each potentially being an expression again

 
  // # Something I thought might potentially work, but does not 🙊
  // I guess this is very wrong from a grammar/semantics PoV
  return Sample(a: value ?? Never);
}

The same issue should apply for optional positional parameters.

Did not have the time yet to fully read on the Pattern language feature draft, but I can imagine that there might be some ”connection“ in the long run.

@Wikiwix Wikiwix added the request Requests to resolve a particular developer problem label Jan 13, 2021
@Hixie
Copy link

Hixie commented Jan 14, 2021

Might be interesting to have a keyword that means "insert whatever value the receiver would use as a default here if we didn't say anything".

@Wikiwix
Copy link
Author

Wikiwix commented Jan 14, 2021

And on second thought staying with the default if the expression evaluates to null is a bad idea, because that means functions like this are harder to use if you potentially want to give null. Though I guess this is not the most regular situation:

void foo([int? bar = 0]) {};

@Hixie
Copy link

Hixie commented Jan 14, 2021

Yeah, null already means something different, so it'd be a pretty significant breaking change to make "null" mean "use the default" here.

@rrousselGit
Copy link

What about the following instead?

class Sample {
  const Sample({int? a}): a = a ?? 42;
  final int a;
}

We could have a shorthand for this:

class Sample {
  const Sample({this.a ??= 42});
  final int a;
}

@Wikiwix
Copy link
Author

Wikiwix commented Jan 14, 2021

@rrousselGit Your suggestion only works for the case where the actual parameter is non-nullable, because otherwise how to know whether nothing was given or null was given?

One other downside of this (as this is already a possible workaround now) is that you will no longer see the default from outside the function (that could be omitted with your second suggestion I guess.

@Hixie's suggestion seems more attractive to me as this might increase the expressiveness of the language overall (perhaps there are other cases where this can become relevant 🤔)

@rrousselGit
Copy link

rrousselGit commented Jan 14, 2021

The problem with "insert whatever value the receiver would use as a default here if we didn't say anything" is that's not the default behaviour when using composition

Take the following code as example:

class SomeWidget extends StatelessWidget {
  SomeWidget({this.a = 42});
  final int? a;
  ...
}

class ComposedWidget extends StatelessWidget {
  ComposedWidget({this.a});
  final int? a;
  
  @override
  Widget build(BuildContext context) {
    return SomeWidget(a: a);
  }
}

Then, when writing ComposedWidget(), this will not use the default value of SomeWidget but null instead

This is a common mistake that developers make.

To work around that, in my projects I've stopped using default values and null entirely, in favour of ?? and unions.
So for example, with TextField.maxLines which is nullable but have a default value, I'd have:

TextField({ Option<int>? maxLines }): maxLines = maxLines ?? const Option(1);

final Option<int> maxLines

This covers all the cases and avoid mistakes.

As for documentation, the dartdoc can mention what the default value is.

@Wikiwix
Copy link
Author

Wikiwix commented Jan 14, 2021

I might be mixing up syntax and semantics now, but if there was a keyword meaning nothing given this might be useful in other occasions and supports what @tatumizer proposes by

foo(giveValue ? (x: value) : newKeyWord) {};
// but I guess "x:value" is not actually something an expression can return so this syntax might not make sense

@tusharsadhwani
Copy link

tusharsadhwani commented Jan 19, 2021

One interesting possibility is the second step: after introducing cond ? x : default expression, define null-elimination ?expr as a shortcut to x != null ? x : default.

foo(x != null? x : default);
foo(?x); // same thing

I'd be pretty happy with this solution.

The distinction between "null" and "no value" is still pretty subtle, like how javascript settled on undefined, which I personally consider pretty bad. The idea of default seems a lot more clear to me.

@lrhn
Copy link
Member

lrhn commented Jan 19, 2021

The usual issues come up with this:

foo([int x = -1, int y = -1]) => ...;
...
  int? x = ..., y = ...;
  foo(x ?? default, y ?? default)

Here we pass either the value of x or nothing (which becomes the default), and we pass either y or nothing.
This makes it possible to pass y and get a default for x.
Or just, directly, allows you to call foo(default, 42), omitting the first argument and passing the second.

I'm fairly sure there is code out there which would be very surprised by getting a default value for the first argument and a non-default for the second. Example:

int hash([Object o1 = _sentinel, Object o2 = _sentinel, Object o3 = _sentinel, ...) { 
  if (identical(_sentinel, o1)) return 0;
  if (identical(_sentinel, o2)) return o1.hashCode;
  if (identical(_sentinel, o3)) return hash2(o1, o2);
  // ...
}

Here, the sentinel is a value that you can be sure is not passed as an explicit argument, and since you cannot pass a later positional argument without passing all the ones before, it doesn't have to check ahead and see whether o2 is a non-sentinel if o1 is one.

That kind of code would break with this change.

(That said, I'd be all for allowing you to omit earlier optional parameters already, like foo(, 2), no default needed. That code would then just have to document that it hashes up to the first omitted parameter).

@lrhn
Copy link
Member

lrhn commented Jan 20, 2021

Then my messaging is right on point. 😁
I don't know whether I think it's a good idea or not. I can see advantages and disadvantages, and the exact details of the design might change the weight one way or another. I just want all the potential complications to be known to everybody in the conversation.

I worry that a feature only for parameters might be too specific. If there are other places where "nothing" makes sense (like in a list literal), would it be better to have the same syntax? If we go with ?e as an expression which evaluates to the same as e if e is not null, and to "nothing' if e is null, we can use it in any location where "nothing" makes sense: Omitted parameter, no entry in collection (because [1, ?e, 3] is much shorter than [1, if ((tmp = e) != null) tmp, 3]), ... and maybe nothing else, which could mean that it's still not general enough.

@lrhn
Copy link
Member

lrhn commented Jan 21, 2021

If the language permits writing a conditional element of the list, it's only natural to allow the same in parameter list, no?

Absolutely not natural, no. The difference between a list (an arbitrary-length sequence of values of the same type) and an argument list (a fixed-length sequence of values of different types) means that omitting an entry at run-time means completely different things.

In a list literal, omitting an element at any point just makes the list shorter, and moves all later element up by one position.

In an argument list, physically omitting an entry moves all later arguments up by one position, which may change the required type.

So, "omitting" in an argument list cannot mean the same thing as in a list literal. It must mean (if it exists at all) that that argument is not passed, but later arguments retain their position.
Or maybe it just means that it's not the same thing at all, and it shouldn't use the same syntax because it might confuse users.

@lrhn
Copy link
Member

lrhn commented Jan 21, 2021

In API and language design, we try to avoid having the same words mean different things. We don't always succeed.
The phrase goes something like "similar things should look similar, and things that look different should be different".
That allows the programmer to leverage their intuition about one thing in other similar cases, and not get confused in cases that aren't similar. If .foo() means approximately the same in many different cases, then adding a new class where foo() means something completely different is bad design. So is using bar() for the thing that people will expect to use foo() for. Don't break users' expectations (which I guess is a usability guideline in general).

The question here is then how similar x != null ? x : default and if (x != null) x really are. They're both "use x if it's not null, otherwise do the default thing" and the "do the default thing" is is subtly different in the two cases, but still somewhat related ("as if it wasn't here").

(I'd allow you to write foo(1,,3) instead of foo(1, default, 3) too, but default is more powerful than just omitting because you can do arbitrary conditions like y == x ? 0 : default (no null involved), so default might still be needed).

Alternatively, allow if (y == x) 0 as an expression in optional argument position. That would be similar to what we do in list literals, and then x? would always mean if (($tmp = x) != null) $tmp. That feels more coherent to me, and doesn't need any default expression.

@tusharsadhwani
Copy link

A pretty simple (yet hard) way to deal with this would be to go the javascript way: another sentinel value, apart from null.

in javascript, passing undefined to a function is the same as not passing anything at all.

@lrhn
Copy link
Member

lrhn commented Jan 21, 2021

Using skip is a(nother) magical name, so we'd probably have to make it a reserved word. That's a high bar (especially since Iterable.skip would become invalid). At least default is already reserved.
If we only make it contextually reserved, so it is only special if it occurs in tail position of an expression in argument position, then that's somewhat ameliorated, but it again means that it's a highly specialized syntax for just that one thing, which means that it's probably not worth the lexical complexity. (The parser would need to know whether skip would be the special operator or just a variable name, so would people reading the code). I think I'd prefer default because it's already recognizable as a reserved word, and therefore stands out more to the reader.

@esDotDev
Copy link

esDotDev commented Apr 16, 2021

foo(?x); doesn't feel readable, and the ? is really starting to get overloaded imo. The full null-check is overly verbose.

Why can't we keep it simple with existing null aware operators??
foo(x ?? default)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests

6 participants