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

If-variables #1201

Open
munificent opened this issue Sep 2, 2020 · 68 comments
Open

If-variables #1201

munificent opened this issue Sep 2, 2020 · 68 comments
Labels
feature Proposed language feature that solves one or more problems field-promotion Issues related to addressing the lack of field promotion nnbd NNBD related issues

Comments

@munificent
Copy link
Member

munificent commented Sep 2, 2020

This is a proposal for how to handle the lack of field promotion with null safety (though it covers more than just that).

The big hammer in the language for making nullable types usable is flow analysis and type promotion. This lets the imperative code that users naturally write also seamlessly and soundly move nullable variables over to the non-nullable type where the value can be used.

Unfortunately, this analysis isn't sound for fields and getters, so those do not promote:

class C {
  Object obj;
  test() {
    if (obj is int) obj + 1; // Error. :(
  }
}

One option is to enable promotion in the cases where using the field is sound, but the boundary there is subtle, it's easy to move a variable across it, and may be too narrow to cover most cases. Another option is to automatically promote fields when failure to do so would cause a static error. That trades static failures, which let users know their code is unsound, with a runtime error that could cause their program to crash.

Given that the trend in Dart is away from code that may silently fail at runtime, I'm not enthusiastic about the latter approach. This proposal describes a feature called "if-variables" that is local, sound, efficient, explicitly opted in (while being concise), cannot fail at runtime, and covers a larger set of painful cases than any of the other proposals.

It looks like this:

class C {
  Object obj;
  test() {
    if (var obj is int) obj + 1; // OK!
  }
}

Basically, take the code you would write today that doesn't promote and stick a var (or final) in front of the if condition. That keyword means "if the type test succeeds, bind a new local variable with the same name but with the tested type". In other words, the above example is roughly syntactic sugar for:

class C {
  Object obj;
  test() {
    if (obj is int) {
      var obj = this.obj as int;
      obj + 1; // OK!
    }
  }
}

This binds a new local variable. That means reading it later does not read the original backing field and assigning to it does not assign to the field, only to the local variable. This is what makes the proposal efficient and sound. The var keyword should hopefully make it clear enough that there is a new local variable in play.

Promoting on null checks

You can also use if-var with nullability checks:

class C {
  int? n;
  test() {
    if (var n != null) n + 1; // OK!
  }
}

Promoting getters

The tested value can be any expression as long as it ends in a named getter:

class C {
  List<Point<num>> points = [Point(1, 2)];
  test() {
    if (var points[0].x is int) x.isEven; // OK!
  }
}

In this case, the last identifier in the selector chain is the one whose name is used for the newly bound variable. The expression is only evaluated once, eagerly, and the result is stored in the new variable.

So not only does this let you promote a getter, it gives you a very nice shorthand to access the value repeatedly.

Negative if-vars

The above examples all test that some value has a promotable type. You can also test that the variable does not have the type and then exit:

class C {
  Object obj;
  test() {
    if (var obj is! int) return;
    obj + 1; // OK!
  }
}

When using is! and == null, the then branch of the if statement must exit by return, throw, etc. The newly-bound variable goes into the block scope surrounding the if statement and continues to the end of the block. In other words, the desugaring is something like:

class C {
  Object obj;
  test() {
    int obj;
    if (obj is! int) return;
    obj = this.obj as int;
    obj + 1; // OK!
  }
}

Proposal

There are basically two separate statements here:

  • A positive if-var that uses is or != null in the condition and scopes the new
    variable only inside the then branch. It's somewhat like if-let in Swift.

  • A negative if-var that uses is! or == null in the condition and scopes the new variable to the code after the if statement. It's akin to guard-let in Swift.

Here is a somewhat more precise description. We change the grammar like so:

ifStatement         ::= "if" "(" expression ")" statement ( "else" statement )?
                      | positiveIfVariable
                      | negativeIfVariable

positiveIfVariable  ::= "if" "(" ifVariable positiveTest ")" statement ( "else" statement )?
negativeIfVariable  ::= "if" "(" ifVariable negativeTest ")" statement

ifVariable          ::= ( "var" | "final" ) ifValue
ifValue             ::= ( ( primary selector* | "super" ) ( "." | "?." ) ) ? identifier
positiveTest        ::= receiver? identifier ( "is" typeNotVoid | "!=" "null" )
negativeTest        ::= receiver? identifier ( "is" "!" typeNotVoid | "==" "null" )

As far as I know, this is unambiguous and compatible with the existing grammar.

Positive if variables

It is a compile time error if the then statement is a block that declares a local variable whose name is the same as the identifier in ifValue. In other words, the new variable goes in the same block scope as the then block and you can't have a collision.

To execute a positiveIfVariable:

  1. Evaluate the expression ifValue to a value v.
  2. Use that value to perform the appropriate type or null test in the positiveTest. If the result is true:
    1. Create a new scope and bind the identifer from ifValue to v.
    2. Execute the then statement in that scope.
    3. Discard the scope.
  3. Else, if there is an else branch, execute it.

Negative if variables

It is a compile time error if the end of the then statement is reachable according to flow analysis.

It is a compile time error if the block containing the if-var statement declares a local variable whose name is the same as the identifier in ifValue. The scope of the declared variable begins before the if-var statement and ends at the end of the surrounding block. The variable is considered definitely unassigned inside the then branch of the if-var statement and definitely assigned afterwards.

To execute a negativeIfVariable:

  1. In the current scope, declare a new variable named with the identifer from ifValue.
  2. Evaluate the expression ifValue to a value v.
  3. Use that value to perform the appropriate type or null test in the negativeTest. If the result is true:
    4. Execute the then statement.
  4. Else:
    5. Assign v to the variable.

Questions

Compatibility?

Since this claim new currently-unused syntax, it is backwards compatible and non-breaking. We can add it before or after shipping null safety.

Is the local variable's type declared to be the promoted type or promoted to it?

In other words, is the desugaring like:

class C {
  Object obj;
  test() {
    if (obj is int) {
      int obj = this.obj as int;
    }
  }
}

Or:

class C {
  Object obj;
  test() {
    if (obj is int) {
      Object obj = this.obj as int;
    }
  }
}

I suggest the former. Mainly because this prevents assigned an unexpectedly wide type to the local variable. Attempting to do so likely means the user thinks they are assigning to the original field and not the shadowing local variable. Making that a static error can help them catch that mistake.

What about pattern matching?

You can think of this feature as a special pattern matching construct optimized for the common case where the value being matched and the name being bound are the same. I think it's unlikely that this syntax will clash with a future syntax for pattern matching, even if we allow patterns in if statements. The var foo is Type syntax is pretty distinct because it mixes both a little bit of an expression and a bit of a pattern.

What about other control flow combinations?

The positive and negative forms allowed here don't cover every possible valid combination of control flow, scoping, and unreachable code. In particular, we could also allow:

class A {
  Object obj;
  test() {
    if (var obj is! int) {
      ...
    } else {
      obj; // If-variable in scope here.
    }
    // And not here.
  }
}

This isn't particularly useful. You can always swap the then and else cases and turn it into a positive conditional variable.

Also:

class B {
  test() {
    if (var obj is! int) {
      return;
    } else {
      obj; // If-variable in scope here.
    }
    obj; // And also here.
  }
}

There's no real value in allowing an else clause when the then always exits. You can just move the code out of the else to after the if.

Finally:

class C {
  test() {
    if (var obj is int) {
      obj; // If-variable in scope here.
    } else {
      obj; // Definitely unassigned here?
      return;
    }
    obj; // If-variable in scope here too.
  }
}

This one is particularly confusing, since there's a region in the middle where you really shouldn't use the variable.

I don't propose we support these forms. I want it to be clear to users when the conditional variable is scoped to the if statement's then branch and when it goes to the end of the surrounding block. The fewer forms we support, the easier it is for users to understand that.

@munificent munificent added the feature Proposed language feature that solves one or more problems label Sep 2, 2020
@munificent
Copy link
Member Author

@mnordine
Copy link
Contributor

mnordine commented Sep 2, 2020

Yeah, I love this. Solves a pain point concisely. The only objection that comes to mind is the "Dart always does what you'd expect" or "no surprises" may come into play with the negative if-vars and the scoping involved there. But as you already cite, there's prior art there, and worth the tradeoff imho.

@jamesderlin
Copy link

if (var obj is! int) return;
obj + 1; // OK!

That seems slightly surprising to me since for (var i in iterable) and for (var i = 0; ...) restrict the scope of i to the loop body. Similarly, in C++, if (T x = f()) restricts x to the if body (or to any of its else if or else bodies). And while that's a different language, if there's any possibility that Dart ever might want to allow something similar, it might be confusing if if (var obj is ...) and if (var obj = ...) had different scopes for obj.

@lrhn
Copy link
Member

lrhn commented Sep 3, 2020

I'm not too fond of making the name of the new variable be implicitly the name of the existing variable.
For one thing, it means that it only works for variables (instance or static), but not for arbitrary expressions (like obj as int intobj, #1191, would). It also hides that you are introducing a new variable., which makes it easier to confuse users.

I'd be more inclined towards a full declaration-as-expression: if ((var local = obj) is int) local + 1;
That will simply allow you to introduce a local variable as an expression, and use it in any code dominated by the declaration. The declaration expression would be treated as an expression evaluating to the declared variable, so you can use it to promote that variable.

@jamesderlin
Copy link

FWIW, one thing that I do like about this (and about @lrhn's suggestion elsewhere for an x as T; statement) is that it wouldn't require introducing a new variable name. There are cases where I'd much prefer to intentionally shadow variables than to have multiple variables that are subtly different but with similar (and sometimes awkward) names, opening the door to accidentally using the wrong variable later.

@munificent
Copy link
Member Author

That seems slightly surprising to me since for (var i in iterable) and for (var i = 0; ...) restrict the scope of i to the loop body.

Yeah, it is somewhat surprising. We could not do this. It's not essential to the proposal. But my hunch is that supporting this would cover a lot of cases where users will expect it to work. The corresponding case where you type promote a local variable does promote the variable in the rest of the block.

I'm not too fond of making the name of the new variable be implicitly the name of the existing variable.

It's magical, but that basically is the value proposition of this feature. If you don't mind writing both the expression and a name for it, you can simply make a local variable:

class C {
  Object obj;
  test() {
    var otherName = obj;
    if (otherName is int) otherName + 1; // OK!
  }
}

The main pain point we have today is that having to write that second name is pointless.

For one thing, it means that it only works for variables (instance or static), but not for arbitrary expressions (like obj as int intobj, #1191, would).

Yeah, I think pattern matching should cover cases like these where you do want to come up with a different name.

It also hides that you are introducing a new variable., which makes it easier to confuse users.

My hope is that var or final are visually distinct enough to mitigate this, but, yes, it's a concern.

@leafpetersen leafpetersen added the nnbd NNBD related issues label Sep 3, 2020
@leafpetersen
Copy link
Member

It's magical, but that basically is the value proposition of this feature. If you don't mind writing both the expression and a name for it, you can simply make a local variable:

This really is an important point. It sounds small, but my experience with doing this refactor during migrations is that there's a lot of friction introduced by having to come up with a meaningful name for something where the meaningful name is already in use.

@rrousselGit
Copy link

rrousselGit commented Sep 3, 2020

Maybe we could have both:

Obj value;

if (var number = value; number is int) {
  int another = number; // valid
}

to support complex expressions

And then offer syntax sugar for the common case, as described in the OP?

@jamesderlin
Copy link

Yeah, it is somewhat surprising. We could not do this. It's not essential to the proposal. But my hunch is that supporting this would cover a lot of cases where users will expect it to work.

I do think the ability to declare the variable in that outer scope would be very helpful to avoid needing an extra level of indentation.

Taking a step back, is something like:

if (var obj is int) {
  ...
}

that much better than:

some_new_redeclaration_syntax_for_obj;
if (obj is int) {
  ...
}

?

If we had some way to declare a local version of a variable, I think that we'd get the same benefit without much loss of convenience, and the variable scope would be clear. Throwing some ideas out:

with var obj;
var obj = obj; // (I've always wanted C to allow this so that macros could be hygienic)

Or introducing new, context-dependent keywords:

local var obj;

@jakemac53
Copy link
Contributor

jakemac53 commented Sep 4, 2020

The difference in scoping between the scenarios is what gives me the most hesitation, although I do agree there are some common scenarios that wouldn't work if you don't add the variable to the outer scope for is!.

One possible resolution would be to always define the variable in the outer scope, in either scenario. That is universally consistent, and it works for all the other control flow combinations that you listed. I think it is a bit unexpected, but we have that case anyways with the specified is! behavior, so it feels like just making that consistent is better.

Edit: Actually the more I think about it the more I think that having it available in the outer scope in either case seems like a real problem. It has a high probability of introducing bugs if people expect it to only be defined for that inner scope - since it is now shadowing the field.

@lrhn
Copy link
Member

lrhn commented Sep 4, 2020

Our "scopes" are currently mostly following block statement structure, with exceptions:

  • for (var i ....) { ... } - only scoped to the for statement, and
  • catch (e, s) { ... } - only scoped to the following block.

That's probably just an accident of specification, we can otherwise only declare variables as declaration-"statements", and scopes only matter where they can contain declarations. We do introduce new scopes for the bodies of loops and branches of if statements, even when they are not explicitly block statements.

For example, I don't think label: var x = e; introduces a new scope, even though it is a composite statement.

It wouldn't be too surprising if we defined that all composite statements introduce a new scope covering the entire constructor (perhaps except labeled statements), so while (test) { ... } would have a new scope covering test and body, which only matters if you can declare a variable in test.

If we have expressions introducing variables, that would make a kind of sense. On the other hand, it would preclude potentially useful code like:

if (_instanceVar is! int x) throw "Wot?";
x.toRadixString(16); // Code is dominated by positive edge of `is int x` test.

@hpoul
Copy link

hpoul commented Sep 8, 2020

I personally like the way this works in swift:

    if let movie = item as? Movie {
        print("Movie: \(movie.name), dir. \(movie.director)")
    } else if let song = item as? Song {
        print("Song: \(song.name), by \(song.artist)")
    }

with dart syntax maybe something like

  if (final movie = item as? Movie) {
    ...
  }

i think as? is a bit more clear than is, it would also be nice if the syntax if (final ...) would generally be true if evaluated to non-null, so you could also use

String? property;
if (final property = property) {
  // property is non-null
}

@natebosch
Copy link
Member

I'm not too fond of making the name of the new variable be implicitly the name of the existing variable.

It's magical, but that basically is the value proposition of this feature. If you don't mind writing both the expression and a name for it, you can simply make a local variable:

For static class variables we can write var someName = ThisClass.someName; and for instance variables we can write var someName = this.someName;. The other situations are local variables which couldn't promote for some other reason or top level variables. Do those latter situations come up often enough that the pain of finding a second name is worth this feature? My preference would be for a more general solution that works for other expressions.

@eernstg
Copy link
Member

eernstg commented Sep 9, 2020

I made an attempt to combine two nice properties: The idea from this proposal that we may introduce a variable with an existing name without repeating that name, and the idea from #1191 'binding type cast and type check' that the introduction of a new name should be usable in a general expression. The resulting proposal is #1210 'binding expressions'.

@munificent
Copy link
Member Author

One possible resolution would be to always define the variable in the outer scope, in either scenario. That is universally consistent, and it works for all the other control flow combinations that you listed. I think it is a bit unexpected, but we have that case anyways with the specified is! behavior, so it feels like just making that consistent is better.

No, that doesn't work because you'll end up with access to a variable whose type does not match its value:

class C {
  Object obj;
  test() {
    if (var obj is int) obj + 1; // OK!
    obj + 1; // If the local obj is still in scope here, what is its type?
  }
}

We could maybe say it's still in scope but un-promote it back to the shadowed member's declared type. So after the if, it demotes back to Object. But that doesn't seem useful. You don't get any useful affordances and now assigning to it just assigns to the shadowed local and not the outer member.

@munificent
Copy link
Member Author

with dart syntax maybe something like

  if (final movie = item as? Movie) {
    ...
  }

I agree that this would be a useful feature and is something I hope we can cover with pattern matching. But this forces you to write the name twice. My goal with this proposal was to give you a nice way to reuse the same name in the very common case where that's what you want.

@hpoul
Copy link

hpoul commented Sep 10, 2020

My goal with this proposal was to give you a nice way to reuse the same name in the very common case where that's what you want.

hmm.. it's just repeated variable name basically, but if you want to save on typing something like kotlins (item as? Movie)?.apply { print(director) } you'd get rid of the item altogether in scope 😅️ sorry just fantasising, probably not feasible in dart.. OP sounds good, everything in that direction 👍️

@jakemac53
Copy link
Contributor

We could maybe say it's still in scope but un-promote it back to the shadowed member's declared type. So after the if, it demotes back to Object. But that doesn't seem useful. You don't get any useful affordances and now assigning to it just assigns to the shadowed local and not the outer member.

Right, this is basically what I would expect. Basically, it would allow you to think about if (var obj is int) desugaring to just:

var obj = this.obj;
if (obj is int) ...

So within the if it would be promoted, and outside of that it wouldn't be.

I would ultimately prefer that the variable wasn't available at all outside the if block though, it doesn't feel nearly as intuitive as the rest of darts scoping rules.

@munificent
Copy link
Member Author

I would ultimately prefer that the variable wasn't available at all outside the if block though, it doesn't feel nearly as intuitive as the rest of darts scoping rules.

Yeah, that's definitely an option. We could simply not support the if (var foo is! Bar) ... form at all.

But my experience from type promotion is that that would be annoying. Type promotion used to only promote inside the body of ifs and not in the continuation and it was a constant annoyance to have to take code like:

bool operator ==(other) {
  if (other is! Point) return false;
  return x == other.x && y == other.y;
}

And turn it into:

bool operator ==(other) {
  if (other is Point) return {
    return x == other.x && y == other.y;
  }
  return false;
}

Because the flow analysis wasn't smart enough to see that those two are identical. That's the main reason I propose supporting both forms: I think there is code that looks natural in both styles and it would feel annoying if only one worked.

I do agree the scoping is surprising in the negative form. I'm just not sure if it's weird enough to sink the proposal.

@leafpetersen
Copy link
Member

@munificent I would like to explore what some actual code looks like under these various proposals. Here's an example piece of code that I migrated, which I think suffers from the lack of field promotion. This is from the first patchset from this CL - I subsequently refactored it to use local variables, the result of which can be seen in the final patchset of that CL. This was a fairly irritating refactor, and I don't particularly like the result. Would you mind taking a crack at showing how this code would look under your proposal?

// The type of `current` is `Node`, and the type of the `.left`  and `.right` fields is `Node?`.
while (true) {
      comp = _compare(current.key, key);
      if (comp > 0) {
        if (current.left == null) break;
        comp = _compare(current.left!.key, key);
        if (comp > 0) {
          // Rotate right.
          Node tmp = current.left!;
          current.left = tmp.right;
          tmp.right = current;
          current = tmp;
          if (current.left == null) break;
        }
        // Link right.
        right.left = current;
        right = current;
        current = current.left!;
      } else if (comp < 0) {
        if (current.right == null) break;
        comp = _compare(current.right!.key, key);
        if (comp < 0) {
          // Rotate left.
          Node tmp = current.right!;
          current.right = tmp.left;
          tmp.left = current;
          current = tmp;
          if (current.right == null) break;
        }
        // Link left.
        left.right = current;
        left = current;
        current = current.right!;
      } else {
        break;
      }
    }

@munificent
Copy link
Member Author

OK, here's what I got, with comments on the changes:

while (true) {
  comp = _compare(current.key, key);
  if (comp > 0) {
    if (var current.left == null) break;  // add "var", negative form.
    comp = _compare(left.key, key);       // "current.left!" -> "left"
    if (comp > 0) {
      // Rotate right.                    // remove "tmp" (use "left" as temp)
      current.left = left.right;          // "tmp" -> "left"
      left.right = current;               // "tmp" -> "left"
      current = left;                     // "tmp" -> "left"
      if (current.left == null) break;
    }
    // Link right.
    right.left = current;
    right = current;
    current = current.left!;              // unchanged, "left" could be stale
  } else if (comp < 0) {
    if (var current.right == null) break; // add "var", negative form.
    comp = _compare(right.key, key);      // "current.right!" -> "right"
    if (comp < 0) {
      // Rotate left.                     // remove "tmp" (use "right" as temp)
      current.right = right.left;         // "tmp" -> "right"
      right.left = current;               // "tmp" -> "right"
      current = right;                    // "tmp" -> "right"
      if (current.right == null) break;
    }
    // Link left.
    left.right = current;
    left = current;
    current = current.right!;             // unchanged, "right" could be stale
  } else {
    break;
  }
}

I'm not 100% sure I did that right.

This is a really useful exercise. I might try to dig up some other migrated code and see how it would look if we had this.

@jodinathan
Copy link

this looks very interesting, however, I find the var in the sentence misleading.

In some other issue I commented about using a new keyword use to add variables to the scope.
It is a little more verbose than the var this proposal, but I find easier to read as it explicitly tells what is happening and why it is needed. It could be also used in other situations like type promotion and any other kind of conditional expression.

In the code above, @munificent comments on the use of var in the if condition and uses the word use to explain how it works.
// remove "tmp" (use "left" as temp)

If anyone found this interesting, help me creating a proposal.
I have ADHD so creating big, nice texted, unfortunately, is not my thing.

this is the same piece of code but tweaked to show some more usage of the use keyword:

while (someObject.someProp != null use obj) { // you can also use the keyword use here
  generic = obj._someMethod(current.key, key); // obj was added to this scope in the line above

  if (generic is Foo use current) { // add a current local variable that is Foo
    if (current.left == null use left) break;  // add a local var *left* within (comp > 0) scope

    comp = _compare(left.key, key);       // "current.left!" -> "left"

    if (comp > 0) {
      // Rotate right.                    // remove "tmp" (use "left" as temp)
      current.left = left.right;          // "tmp" -> "left"
      left.right = current;               // "tmp" -> "left"
      current = left;                     // "tmp" -> "left"
      if (current.left == null) break;
    }

    // Link right.
    right.left = current;
    right = current;
    current = current.left!;              // unchanged, "left" could be stale
  } else if (generic is Bar use current) { // current in this scope is set to a Bar type
    if (current.right == null use right) break; // add "var", negative form.

    comp = _compare(right.key, key);      // "current.right!" -> "right"

    if (comp < 0) {
      // Rotate left.                     // remove "tmp" (use "right" as temp)
      current.right = right.left;         // "tmp" -> "right"
      right.left = current;               // "tmp" -> "right"
      current = right;                    // "tmp" -> "right"
      if (current.right == null) break;
    }

    // Link left.
    left.right = current;
    left = current;
    current = current.right!;             // unchanged, "right" could be stale
  } else {
    break;
  }

}

@jodinathan
Copy link

jodinathan commented Sep 24, 2020

I agree with @jakemac53 about the scope of the if-variable var, so I tweaked @leafpetersen's code to how I think is the most organized and easy to read option:

    while (true) {
      comp = _compare(current.key, key);

      if (comp > 0) {
        if (current.left != null use left) {
          if (_compare(left.key, key) > 0) {
            // Rotate right.
            current.left = left.right;
            left.right = current;
            current = left;

            if (current.left == null) { 
	      break;
            }
          }

          // Link right.
          right.left = current;
          right = current;
          current = current.left!;
	} else {
	  break;
	}
      } else if (comp < 0) {
        if (current.right != null use right) {
          if (_compare(right.key, key) < 0) {
            current.right = right.left;
            right.left = current;
            current = right;

            if (current.right == null) { 
              break;
            }
          }

          // Link left.
          left.right = current;
          left = current;
          current = current.right!;

	} else {
          break;
        }
      } else {
        break;
      }
    }

We could also give the option to only add the keyword use so the name of the variable keeps the same, ie:

if (current.right != null use) 

But I am really not sure if I like it. Seems too magical to me and I like explicit stuff.

*edit: I pondered about the alone use above and I liked it.
One option is to add another keyword for it:

if (current.right != null useIt)

now I have the right variable without having to write down "right" and it is clear and concise.

@leafpetersen leafpetersen added the field-promotion Issues related to addressing the lack of field promotion label Dec 8, 2020
@jefflim-google
Copy link

jefflim-google commented Oct 19, 2021

+1 to the idea of if-variables.

One thought: would 'final' be more aligned given its existing meaning, or would 'var' and 'final' both be usable, where 'var' would allow writing to the local variable of the restricted type? Having 'a' be assigned inside the statement seems like it could lead to confusing code.

if (final a != null) {
  // Use a as non-null, but assignment to 'a' is not allowed.
}

@munificent
Copy link
Member Author

would 'final' be more aligned given its existing meaning, or would 'var' and 'final' both be usable

The proposal allows both var and final, yes.

@jefflim-google
Copy link

If 'var' is used, does writing to it affect the underlying value, or a shadow copy of it? From the proposal, it seems like a local copy.

if (var someObject.a != null) {
  a = 1; // Seems like this just writes to a local 'a'.
}

In this case, any thoughts on how private variable names should work?

if (final _fieldValue != null) {
  // `_fieldValue` or `fieldValue` here?
}

Using a local fieldValue (rather than _fieldValue) here would help reinforce that it is a local variable that doesn't affect the underlying variable, and help to remain aligned with effective dart's "DON’T use a leading underscore for identifiers that aren’t private."

@lrhn
Copy link
Member

lrhn commented Oct 28, 2021

You introduce a new variable with the chosen name. That means it shadows the original thing you read.
If you want to write back to the original, you need to write someObject.a = 1;.

The new variable has the same name as the variable (or getter) that was read to create it.
That's the variable that you now have the value of, and therefore don't need to read again.
If we changed the name from _fieldVariable to fieldVariable, we'd now potentially shadow another variable named fieldVariable, and we'd then need a way to avoid that. It's much simpler to just use the same name.
(Also, we never treat name and _name as being related in any other place, they are different names, as different as name1 and name2.)

@jefflim-google
Copy link

jefflim-google commented Oct 28, 2021

(Also, we never treat name and _name as being related in any other place, they are different names, as different as name1 and name2.)

Perhaps going off on a tangent, but might this. constructor forms also potentially have this kind of relationship in the future for private initialization?

class MyClass {
  MyClass.positional(this.value);
  MyClass.optionalPositional([this.value]);
  MyClass.named({required this.value});
  MyClass.optionalNamed({this.value});

  final int value;
}

But for privates:

class MyClass {
  MyClass.positional(this._value);
  MyClass.optionalPositional([this._value]);

  // Not possible: MyClass.named({required this._value});
  // Not possible: MyClass.named({required this.value});
  MyClass.named({required int value}) : _value = value;

  // Not possible: MyClass.optionalNamed({this._value});
  // Not possible: MyClass.optionalNamed({this.value});
  MyClass.optionalNamed({int value}) : _value = value;

  final int _value;
}

Note: Field parameters shadowing other variables can happen, even in the current proposal.

var a = 0;
if (var otherObject.a != null) {
  // No way of accessing var a above here.

Though I readily admit keeping underscore for the local is probably easier conceptually and implementation wise.... just writing some random thoughts.

@leafpetersen
Copy link
Member

I do wonder whether we want to either make these variables always final by default, or only allow the final form. It feels to me like there's a nasty footgun here when you're dealing with fields from your own class. It feels extremely easy to forget that you've shadowed a field with a variable and to try to write back to it:

class C {
  Object obj;
  test() {
    if (var obj is int) {
       obj = obj + 1; // PROBABLY NOT WHAT YOU WANTED TO DO!
    }
  }
}

@Levi-Lesches
Copy link

That sounds a lot like the shadow proposal, #1514. As an example:

class C {
  Object obj;

  void test() {
    shadow obj;  // references to obj are local, but are synced to the field
    if (obj is int) obj++;  // increments both the local variable AND the field
  }
}

@jodinathan
Copy link

I do wonder whether we want to either make these variables always final by default, or only allow the final form. It feels to me like there's a nasty footgun here when you're dealing with fields from your own class. It feels extremely easy to forget that you've shadowed a field with a variable and to try to write back to it:

class C {
  Object obj;
  test() {
    if (var obj is int) {
       obj = obj + 1; // PROBABLY NOT WHAT YOU WANTED TO DO!
    }
  }
}

Quick thinking this seems an exception instead of the common case.

@jodinathan
Copy link

I liked the idea but this is so weird to read:

if (final o=obj, o1=obj1; o is int && o1 is int) {
   // use o, o1
}

I think we should make it a suffix because that is how our brain separates actions, ie: do this then do that.

if (obj is int use obj && arg != null use safeArg) {
   // the new local obj is final and an int
   // the new local safeArg is final and non null
}

Because of the word use, which is a verb (denotates action) and it is a suffix, I think there is very little chance to the above mislead in anyway on what is happening.

But it could be var or final:

if (obj is int var obj && arg != null final safeArg) {
   // the new local obj is variable and an int
   // the new local safeArg is final and non null
}

@jodinathan
Copy link

Used C/C++ a lot 15 years ago. I wouldn't choose all things from it blindly.

if (final o=obj, o1=obj1; o is int && o1 is int) {
   // use o, o1
}

Would you mind exposing the advantages here?

For me is basically the same amount of work that we already do:

 final o=obj, o1=obj1;

 if (o is int && o1 is int) {
    // use o, o1
 }

In fact, reading again, I find the current Dart syntax better to read than the C++ version.

@ykmnkmi
Copy link

ykmnkmi commented Nov 12, 2021

Similar to Python and Go walrus operator, but shorter, like labeled blocks:

if (final user: object is User && /* var */ data: user.data /* != null */) {
  data = callback(data);
  // ...
  send(user.email, messageFor(data));
}

@jodinathan
Copy link

Similar to Python and Go walrus operator, but shorter, like labeled blocks:

if (final user: object is User && /* var */ data: user.data /* != null */) {  data = callback(data);

IMO it looks like it returns a bool from object is User

@stereotype441
Copy link
Member

C# apparently has a pretty tight syntax for this sort of thing that allows the user to specify a variable name:

if(a.b is int y) {
  doSomethingWithInt(y);
}

@munificent
Copy link
Member Author

C# apparently has a pretty tight syntax for this sort of thing that allows the user to specify a variable name:

if(a.b is int y) {
  doSomethingWithInt(y);
}

Yeah, I considered that. Like @tatumizer notes, it doesn't extend gracefully to == null checks. Also, it does require you to come up with a name for the bound variable, which is both a pro (explicit) and con (redundant).

@rubenferreira97
Copy link

With pattern matching coming out, could we also extend this proposal to allow when-variables?

Example:

enum Difficulty {
  easy(1, "Easy"),
  medium(2, "Medium"),
  hard(3, "Hard");

  static Difficulty? fromId(int id) => Difficulty.values.firstWhereOrNull((d) => d.id == id);

  const Difficulty(this.id, this.designation);
  final int id;
  final String designation;
}

class Game {
  final Difficulty difficulty;
  final int levels;
  final String name;
  Game(this.difficulty, this.levels, this.name);
}

void main() async {
  final json = jsonDecode('{"name": "name, "levels": 100, "difficultyId": 1}');
  if (json case {
    'name' : final String name,
    'levels' : final int levels,
    'difficultyId': final int difficultyId,
    } when (final difficulty = Difficulty.fromId(difficultyId) != null)) {
      final game = Game(difficulty, levels, name);
    // insert game and return 200 ok
   }   
   //return 400 bad request
}

@heralight
Copy link

heralight commented Mar 16, 2023

A more generic way, could be something more like C# 'out' parameter modifier
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/out-parameter-modifier

For example instead of:

final uri = Uri.tryParse(str);
if (uri != null) {
    ...
}

you will have something like:

if (Uri.tryParse(str, out uri)) {
   ... play with uri
}

and the tryparse signature:

bool tryParse(String str, out Uri uri)

best regards,

Alexandre

@lrhn
Copy link
Member

lrhn commented Mar 20, 2023

The C# out parameter is just a reference parameter, with the added requirement that the method does assign to the variable. They allow you to declare the variable in-place.

Which means that it wouldn't work for tryParse which won't assign if it doesn't match. Or rather, the signature would be bool tryParse(String input, out Uri uri); and it will have to assign a dummy Uri to uri even when it doesn't match.

An out parameter makes sense in some APIs, but usually returning a record would be just as good.
The requirement that it's assigned before the call exits means it's not viable for async functions or generators, only functions which return synchronously.

In the next Dart, I'd use patterns:

if (Uri.tryParse(source) case var uri?) {
  // Use uri
}

@heralight
Copy link

The C# out parameter is just a reference parameter, with the added requirement that the method does assign to the variable. They allow you to declare the variable in-place.

Which means that it wouldn't work for tryParse which won't assign if it doesn't match. Or rather, the signature would be bool tryParse(String input, out Uri uri); and it will have to assign a dummy Uri to uri even when it doesn't match.

An out parameter makes sense in some APIs, but usually returning a record would be just as good. The requirement that it's assigned before the call exits means it's not viable for async functions or generators, only functions which return synchronously.

In the next Dart, I'd use patterns:

if (Uri.tryParse(source) case var uri?) {
  // Use uri
}

@lrhn Thank you for pointing me this pattern matching solution.

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 field-promotion Issues related to addressing the lack of field promotion nnbd NNBD related issues
Projects
None yet
Development

No branches or pull requests