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

unify the syntax of if-statements, collection ifs and the ternary operator #729

Open
lhk opened this issue Dec 7, 2019 · 13 comments
Open
Labels
feature Proposed language feature that solves one or more problems

Comments

@lhk
Copy link

lhk commented Dec 7, 2019

These three language constructs are closely related:

  • if-statements
  • ternary operator
  • collection-ifs

I think the development of NNBD, which also introduces new syntax, offers an opportunity to merge the semantics and syntax.

Would it be possible to do something like this:

  • convert if-statements to expressions.
  • deprecate the ternary operator
  • convert collection-ifs to if-expressions

It seems to me that the first two points should be straightforward. All if-statements are now expressions. An if-expression with return type void is essentially an if-statement, this covers existing code.

The introduction of NNBD is probably very much needed for this feature, since an if-expression without an else block will return an optional value. So if the else branch doesn't exist, or if either branch returns a nullable type, the type of the if-expression is nullable.

Based on this, the ternary operator could be deprecated.

Well, I am aware that now my proposal breaks. Collection-ifs are now syntactically identical to an if-expression without an else block. And instead of filtering elements in the collection, they will insert lots of nulls.

As a fix, I would be happy with something like a 'remove-null' operator. Maybe ??.
So the code would look like this:


var nav = [
  'Home',
  if (promoActive) 'Outlet'
]; // should produce a warning about nullable code

List<String?> nav = [
  'Home',
  if (promoActive) 'Outlet'
]; // works

List<String> nav = [
  'Home',
  if (promoActive) 'Outlet'
]; // compiler error

List<String> nav = [
  'Home',
  if (promoActive) 'Outlet'
]??; // works

This language change would introduce if-expressions and merge the syntax of collection-ifs. Also, it would free up the syntax of the ternary operator, which could then be used for null checks. Actually, I think that the graceful deprecation of the ternary operator is the real advantage to be had here. This is directly related to #376

@lhk lhk added the feature Proposed language feature that solves one or more problems label Dec 7, 2019
@lhk
Copy link
Author

lhk commented Dec 9, 2019

@tatumizer thanks for the link, that issue looks very similar.

I don't advocate an operator that magically makes nulls disappear, though. That seems like it could lead to highly unintuitive code.
I think this operator only makes sense on collections without invariant assumptions on their length. So it should be applied on collection types.

What is the overall state of this? It seems important to figure out before nnbd is finalized.

@Cat-sushi
Copy link

This issue consists of three orthogonal issues and one meta issue.
To make things forward, I quarried #740 "If-expression without ? with mandatory else-part" out.

@munificent
Copy link
Member

It seems to me that the first two points should be straightforward. All if-statements are now expressions. An if-expression with return type void is essentially an if-statement, this covers existing code.

What about if statements whose body is a block or some other statement? Are you proposing to make blocks become expressions too? If so, what does this do:

var x = if (true) {} else {};

Is {} an empty block or an empty map?

@rrousselGit
Copy link

rrousselGit commented Dec 15, 2019

Flutter using a lot of named parameters, with many optional ones, I do agree on the idea of having a ìf`expression like with collections.

It's a pain to have to write:

SomeWidget(
  parameter: condition ? value : null,
)

It'd be a lot more readable if we could just write:

SomeWidget(
  parameter:  if (condition) value,
)

@lhk
Copy link
Author

lhk commented Dec 17, 2019

This is valid dart code:

void main() {
  for (int i = 0; i < 5; i++) {
    {print('hello ${i + 1}');} // a block containing statements to be executed
    var a = {}; // an empty map
  }
}

It seems to be possible to distinguish between blocks for scoping and map literals. Could this also be used here? And if the branches contain more than the literal to be returned, I would propose the return keyword to mark the value of the expression.


if(cond) {} // if branch returns an empty map literal, else branch returns null
if(cond) { print(1); } // both if and else return null, compiler warning when the value of this is used
if(cond) { print(1); return 1;} // if branch is expression, return type is int, else branch returns null

I would assume that this could integrate nicely with the new infrastructure of NNBD

if(cond) {} // has type Map?
if(cond) { print(1); } // has type Never
if(cond) { return 1; } // has type int?
if(cond) 1 // has type int?
if(cond) 1 else 2 // has type int

@munificent so I think that yes, I would propose to make blocks expressions, too. And I hope that the parser can disambiguate between blocks and map/set literals.

@rrousselGit
Copy link

rrousselGit commented Dec 17, 2019

Can't we statically differentiate if as expressions from if as statements?

Such as expression ifs can't have blocks (like with collection ifs), but statement ifs can.

As such we have:

Map? result = if (bool) {}

var result = if (bool) {  print('42');  } // compile error

vs:

if (bool) {
  print('42');
}

From my understanding, this shouldn't cause any issue as expressions vs statements ifs don't overlap.

We can't write: () => if (bool) return 42 for example.

@munificent
Copy link
Member

This is valid dart code:

void main() {
  for (int i = 0; i < 5; i++) {
    {print('hello ${i + 1}');} // a block containing statements to be executed
    var a = {}; // an empty map
  }
}

It seems to be possible to distinguish between blocks for scoping and map literals.

Yes. The way the language does that is by forbidding a map/set literal to appear at the beginning of an expression statement. In other words, in a context where the language knows you are writing a statement, it knows { must be the beginning of a block.

Could this also be used here?

Yes, it could. But I worry that the result is a very subtle rule that users would run into frequently but not actually understand. I think it's technically feasible, but that doesn't necessarily mean it's usable in that it is something the majority of the users can easily understand and like.

@rrousselGit
Copy link

I remember seeing @lrhn saying "if expression would be inconsistent with collection if because the later doesn't return null but nothing" (but don't remember where I saw it)

I would argue that an expression if would not return null, because of default values.

Consider:

void function({String value = 'default'}) {
  print(value);
}

then used this way:

function(value: if (condition) 'hello world');

Then I think it would be logical for it to be an equivalent to:

if (condition) {
  function(value: 'hello world');
} else {
  function();
}

This means that if condition is false, then that would print default instead of null.

This also means that an "expression if" has some form of "empty" too, which matches with collections.

@rrousselGit
Copy link

The argument was that it's not clear whether "10" is the first parameter or the second

I can see how people would be mixed between both.
Although having foo(if (cond) 5, 10) do, for optional positional parameters:

  • foo(10)
  • foo(5, 10)
    based on cond seems very dangerous to me.

Say someone first defined foo as: void foo(int a, [int b]) then refactored it to void foo([int a, int b]), then it would be very difficult to evaluate what code is impacted by such change.

@lrhn
Copy link
Member

lrhn commented Feb 20, 2020

@rrousselGit
Also consider how you would type-check that call. Even if you can do something meaningful at run-time when you know the value of cond, you also need to statically check that ... what? Both ways to call it is valid? If you have several conditional arguments, it's an exponential number of combinations to check against the function type.

And as you say, if changing the function's type from void Function(int, [int]) to void Function([int, int]) changes behavior, then it gets really confusing when you assign a function of the latter type to a variable of the former type. Should the static type determine the call behavior?

An expression which evaluates to "nothing", like a missing else branch, can only meaningfully be used in a location where omitting something doesn't matter for the static typing. That's why it makes sense in a list, set or map literals - it's an arbitrary length repetition of things of the same type, so static typing is unaffected by omitting one of the entries. If we had rest parameters, it would make sense there too.

@rrousselGit
Copy link

rrousselGit commented Feb 20, 2020

If you have several conditional arguments, it's an exponential number of combinations to check against the function type.

Would it be possible to introduce a nothing keyword, different from null, that applies the default value then?

Then var value = if (cond) 42 would be equivalent to var value = if (cond) 42 else nothing;

@rrousselGit
Copy link

how will it help to resolve the ambiguity of optional positional parameters?

That means we could write:

foo(nothing, 10)

which means the first parameter gets its default value, and the second one is always 10.

This further means that:

foo(if (cond) 42, 10)

would be equivalent

foo(if (cond) 42 else nothing, 10)

which is consistent with collection ifs and named parameters

@rrousselGit
Copy link

Well, it's a keyword not a value.
We probably shouldn't be able to do things like var x = nothing or [].add(nothing).

So since there's no added value in having nothing in lists, maybe we could just disable [nothing] too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

5 participants