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

Support for Swift style if-let and guard-let constructs. #2074

Open
leafpetersen opened this issue Jan 20, 2022 · 22 comments
Open

Support for Swift style if-let and guard-let constructs. #2074

leafpetersen opened this issue Jan 20, 2022 · 22 comments
Labels
field-promotion Issues related to addressing the lack of field promotion

Comments

@leafpetersen
Copy link
Member

Swift supports two special statement forms for working with optional values (nullable values in Dart): if-let and guard-let. Each construct is a variant of a conditional that scrutinizes an optional (nullable in Dart) value, and binds a value to the underlying value in one of the two continuations if a value is present (not null in Dart). The if-let construct binds the variable in the body of the if, and the continuation has no binding for the variable. The guard-let construct binds the variable in the continuation, and requires that the body of the if never complete (i.e. return, or throw, on all paths). Examples:

func testIfLet(maybeAnInt : Int?) {
  if let definitelyAnInt = maybeAnInt {
    // This code is reached only when maybeAnInt is not nil. 
    // The variable definitelyAnInt is bound to the non-null value of maybeAnInt in this block.
  }
  // definitelyAnInt is not in scope here.
}

func testGuardLet(maybeAnInt : Int?) {
  guard let definitelyAnInt = maybeAnInt else {
    // This code is reached only when maybeAnInt is nil.
    // The variable definitelyAnInt is not in scope in this block.
    //  This block must exit.
    return; 
   }
  // This code is reached only when maybeAnInt is not nil. 
  // The variable definitelyAnInt is bound to the non-null value of maybeAnInt for the rest of this block
}

These constructs are used in Swift to make working with optional (nullable) values easier. In Dart, for local variables we lean on promotion, which allows null checks to "promote" the type of a nullable variable to its non-nullable form. However, this cannot be done for properties on objects in general without implicit runtime checks, which is a pain point in working with null safe code. We have been exploring various approaches to make working with nullable properties easier in Dart. This issue is to track the idea of adding constructs equivalent to if-let and guard let to Dart. It is likely that this might be subset of a larger pattern matching feature, but for completeness this issue tracks the specific use case separately.

cc @munificent @lrhn @eernstg @jakemac53 @natebosch @stereotype441 @kevmoo @mit-mit

@leafpetersen leafpetersen added the field-promotion Issues related to addressing the lack of field promotion label Jan 20, 2022
@leafpetersen
Copy link
Member Author

Some syntax brainstorming.

A fairly direct translation could look something like the following, assuming a variable s of type String?:

// If let 
if! var x = s { // x in scope here }

// guard let
var y = s else! { return; }
// y in scope here

This is compact, but limited to null checks. It would be good to also support instance checks in Dart. Possible generalization:

// if let
if (var x = s) != null { // x in scope here }

(var y = s) != null else { return; }
// y in scope here

if (var a = s) is int { // a in scope here }

(var b = s) is! int { return; }
// b in scope here 

In each case, the semantics is roughly to bind a to s, then do the specified check on a both to decide which continuation to check, and to promote a appropriately.

The above is general, but somewhat verbose for the null checks, so the first syntax above could be added as a shorthand for the null check variants. An alternative shorthand syntax:

// if -let
if (var x = s)! {  //x in scope here }

(var y = 3)! else { return; }
// y in scope here

An alternative would be to decompose this uniformly into a variable binding and a continuation:

// x is in scope in the condition and the block
(var x = s) if (x != null) { // Block executed if condition is true, with promotion
}

// y is in scope in the condition and the continuation, block must exit
(var y = s) else (y == null) { return;
}
// y is in scope here

cc @munificent this sounds fairly similar to what you've been exploring with having a pattern syntax for null checks?

@Levi-Lesches
Copy link

Levi-Lesches commented Jan 20, 2022

This sounds fairly similar to what you've been exploring with having a pattern syntax for null checks?

Yeah, this reminds me of if-vars (#1201), especially @lrhn's comment at the top of that thread:

I'd be more inclined towards a full declaration-as-expression: if ((var local = obj) is int) local + 1;

I like that version, I feel it's more Dart-y, with a nice mix of familiarity and verbosity, but also tersely conveying the point:

final int? s;

// Declaring x as an if-var
if ((var x = s) != null) { /* x is non-nullable here */ }

// Declaring y as a negative if-var such that it is in-scope *after* the block.
// 
// It did receive some pushback (mainly from me, I'll admit) due to it being slightly unintuitive and 
// not following some other Dart patterns. For more, see my comment: 
// https://github.com/dart-lang/language/issues/1201#issuecomment-754972984 
if ((var y = s) == null) { return; } 
// y is in scope here, and is non-nullable

// Declaring a as an if-var
if ((var a = s) is int) { /* a is non-nullable here */ }

// Declaring b as a negative if-var
if ((var b = s) is! int) { return; }
// b is in scope here, and is non-nullable

With such a short general form, I'm not sure null-checking needs a specific shorthand

@leafpetersen
Copy link
Member Author

I'd be more inclined towards a full declaration-as-expression: if ((var local = obj) is int) local + 1;

I like that version, I feel it's more Dart-y, with a nice mix of familiarity and verbosity, but also tersely conveying the point:

Fair enough and so noted, but to cut off too much divergence here, I have quite strong objections to declarations as expressions without explicit scope delineation, that I have elaborated on in the relevant issues. It's still an option, but there's a lot of work to convince me that it's not a terrible idea (and the relevant issue to do that convincing on is one of the issues covering it... :) ).

@Levi-Lesches
Copy link

That's hardly a fair comparison. If-let isn't supposed to be about squishing your code down to less lines, it's about conveniently assigning a new, short name to an already existing value for convenience and readability, none of which are in your example. That'd be much better off as several lines as we're used to:

final int first = int.parse("4");
final int second = int.parse("42");

if (first < second && second < 100) 
  print("$first < $second < 100");

Just because a feature can be abused to be unreadable, doesn't mean it always will or it shouldn't be done that way. (Keep in mind you can technically write all of the above in one line thanks to semicolons. Does that mean semicolons inherently imply bad design?)

@lrhn
Copy link
Member

lrhn commented Jan 20, 2022

I'm not fond of the Swift syntax. It's too implicit for my taste, and, as stated, it only works for null checks. If you had to write the actual != null check, I think it would be easier to read, and you could also do other tests.
It also prevents chaining of conditions, unless you can do if let x = v1 && let y = v2 { ... }.
(Or, I guess you can do if let x = v1 { if let y = v2 { ... }}.)
Because the null check is implicit, you don't get any control over where to put it.

The idea works - introducing a binding in an if condition, and scoping it to one or two branches only.

I'd prefer the syntax we've previously discussed for introducing variables in tests: if (var x = e; test) { ... }.
Like:

Object? foo;
if (var x = foo; x is int) {  // Declaration prior to `;`, boolean condition after, scope is entire `if`.
 // x in scope here as int
} else {
 // x in scope here as Object?
}
// x not in scope here

That too makes the variable declaration part of the if statement (just like a for statement), but it makes the test explicit, so you can choose any promoting test, and even composite tests.

(Do notice that it can be used as a let expression in collection literals, as {if (var x = foo; true) x: x}, so we might as well introduce a let element/expression too. Possibly just make (var x = e1; e2) an expression, parentheses required, where x is only in scope in e2, and special case if to extend that scope to branches if its test is such an expression).

@munificent
Copy link
Member

Do we have any evidence that any of the proposals is a terrible idea? Do we have any evidence either way? So far, it has been all hand-waving and no data. If we look for the data, we can find some evidence that 2-clause if is not problematic: the users don't complain about this feature in golang. Are we sure the users will complain about @ vars? Maybe they will love the feature?

We have to have the ideas before we can get data on them, and this issue tracker is where we first begin discussing a potential feature. Gathering user data is expensive and time-consuming so it's not something we can do freely for every feature idea. We rely on feedback from users here and the taste and judgement of the language team to decide how to spend that UX research budget wisely. One can't simply UX study their way into a good language design. UX studies are great analytical tools, but they don't create features.

In fact, we have no evidence that nnbd itself is net positive for the popularity of dart. If you look into TIOBE index, you might have some doubts. (I have some psychological conjecture why users declare their love for nnbd and then run away when they get it - but this thread is not the right place, and it's too late anyway)

If you find yourself writing "this thread is not the right place", in the future please consider just not writing that comment.

@munificent
Copy link
Member

munificent commented Jan 20, 2022

This is roughly inline with what I've been noodling on too. As Lasse says, I'm not super excited about adding a form like if! var x = s that only helps with null checks but doesn't work with other kinds of promotion or guards.

Pattern-based if

With pattern matching, I would already like to find some kind of syntax to use a single refutable pattern in an if-like constract similar to Rust's if-let (which is not specialized to option-checks like Swift's if-let:

// The `if let` construct reads: "if `let` destructures `number` into
// `Some(i)`, evaluate the block (`{}`).
if let Some(i) = number {
    println!("Matched {:?}!", i);
}

Adapting something like that to Dart is tricky because the pattern proposal I have for Dart follows Swift in that it splits refutable and irrefutable patterns into two different grammars. I think there are good reasons for that, but it means jamming a refutable pattern in an if statement looks weirder.

If we were to just do:

if ( <refutable pattern> = <expr> ) ...

Then it works nicely in cases like:

// Here, `int x` is a pattern that refutes if
// the value is not an int or binds it if it is:
if (int x = someExpression)

But also allows strange code like:

// Literals are valid refutable patterns that refute if not equal:
if (123 = someExpression)

// Identifiers are valid refutable patterns that refute if not equal:
if (math.pi = 3.14)

I think the refutable pattern grammar in the proposal works well in switch cases:

switch (someExpression) {
  case 123: // Literal.
  case math.pi: // Constant.
  case int x: // Type test and bind.
  case [var x, var y]: // Destructure.
}

It just looks odd stuck right inside if. One syntax I've considered is to
reuse case to mean "here comes a refutable pattern:

if (case int x = someExpression) ...
if (case 123 = someExpression) ...
if (case math.pi = 3.14)

I like the symmetry with switch cases, but I don't know if it's too unusual.

Pattern-based guard

I really like guard-let in Swift and think it would be very nice to extend the above to also support a form that binds in the rest of the block. I think it's a good fit for Dart since we already do reachability analysis for promotion so "must exit" blocks are natural to compute.

I don't have any great syntax ideas yet. Maybe:

unless (case int x = someExpression) {
  // This block must exit.
  return "not an int";
}

// x is in scope here and has type int.

Null patterns

Of course, all of the above doesn't get us very far for the "check if not null" case if the pattern for doing that is verbose. Right now, you'd have to do a type test pattern, which can be very verbose for long types:

SomeExtremelyLong<AndAlso, GenericType>? maybeThing = ...;
if (case SomeExtremelyLong<AndAlso, GenericType> thing = maybeThing) ...

That's pretty bad. My current idea is a "null-check" refutable pattern. Syntactically, it's just:

nullCheckPattern ::= pattern `?`

So a postfix ? with any other refutable pattern on the left. It refutes if the value is null. Otherwise, it applies the non-null value to the inner pattern. In common cases where the inner part is an inferred variable, I think it looks pretty nice:

SomeExtremelyLong<AndAlso, GenericType>? maybeThing = ...;
if (case var thing? = maybeThing) // thing has non-nullable type here.

It also composes to let you do other nice stuff like:

List<int>? maybeList = ...
if (case [var x, var y]? = maybeList) {
  // Get here is list was non-null and contained two elements.
}

The corresponding guard form could be something like:

SomeExtremelyLong<AndAlso, GenericType>? maybeThing = ...;
unless (case var thing? = maybeThing) { return 'null!'; }
// thing has non-nullable type here.

TL;DR:

  • Yes, I like the idea of if-let and guard-let like forms for doing null and possibly other promotion.
  • If possible, I'd like to build it on top of pattern matching.
  • I have an idea for a null-check pattern that I think works pretty well.
  • I'm not sure yet on an if and guard form that take patterns. Fitting that into the current proposed pattern syntax is kind of tricky.

@leafpetersen
Copy link
Member Author

unless (case int x = someExpression) {
  // This block must exit.
  return "not an int";
}

One thing that I liked in my syntax noodling above was that the guard-let moved the binding to the front, which made it syntactically easier to see that the variable binding was in the enclosing block, rather than the guarded block. Perhaps the same here?

(case int x = someExpression) else {
  // This block must exit
  return "not an int";
}

It also feels heavy to me to have to use both extra keywords and parentheses. If we do use the case keyword, I wonder if we could get rid of the parens (possibly by requiring braces on the body).

if case int x = someExpression {
  // x bound here
}

case int x = someExpression else {
  // This block must exit
  return "not an int";
}

@munificent
Copy link
Member

One thing that I liked in my syntax noodling above was that the guard-let moved the binding to the front, which made it syntactically easier to see that the variable binding was in the enclosing block, rather than the guarded block.

Yeah, I agree. It feels weird to put the binding inside parentheses because that reads to me like it should be scoped to the else block and not the rest of the surrounding block.

case int x = someExpression else {
  // This block must exit
  return "not an int";
}

I toyed with this syntax too. I like that it doesn't require a block because the else is a delimiter for the expression. The main problem is that it gets weird if you try to use it inside a switch statement:

switch (foo) {
  case 123:
    case int x = someExpression else {
      // This block must exit
      return "not an int";
    }
}

It's probably not technically ambiguous, especially if we require an else, but it potentially requires a lot of lookahead to realize you're not parsing an actual switch case. And I think it's probably pretty hard for a human to piece out what's going on here.

Perhaps we could use with, which is conveniently already a reserved word:

with int x = someExpression else {
  // This block must exit
  return "not an int";
}

That looks... OK... to me. I'm a little hesitant to claim with for this because I could also see us wanting to use it for some kind of scoped resource construct (#2051).

Maybe we shouldn't have a guard-let like construct. It makes a lot of sense in Swift because Swift doesn't do control flow based type promotion. But having a promotion feature that changes the types of existing variables in the rest of a block and a feature that binds new variables in the rest of the block might be too much to pack into a single language.

@leafpetersen
Copy link
Member Author

leafpetersen commented Jan 21, 2022

Is this really too ambiguous?

int x = someExpression else {
  // This block must exit
  return "not an int";
}

Ugly parsing, but it feels like it should be doable?

Alternatively, what about using a variant of = for the binding?

if (int x =~ someExpression) {
}
int x =~ someExpression else {
  // This block must exit
  return "not an int";
}

Or re-use instance checks?

if (someExpression is int x) {
}

someExpression is int x else {
  // This block must exit
  return "not an int";
}

@mnordine
Copy link
Contributor

mnordine commented Jan 22, 2022

List<int>? maybeList = ...
if (case [var x, var y]? = maybeList) {
  // Get here is list was non-null and contained two elements.
}

@munificent your comment, did you mean to say at least two elements?

Or do you really mean 2 and only 2 elements, and you'd need to do something like:

List<int>? maybeList = ...
if (case [var x, var y, ...]? = maybeList) {
  // Get here is list was non-null and contained at least two elements.
}

@munificent
Copy link
Member

munificent commented Jan 25, 2022

Is this really too ambiguous?

int x = someExpression else {
  // This block must exit
  return "not an int";
}

It's not ambiguous but it's very confusing in the context of the current proposal. Right now, the proposal follows Swift in that there are two sets of patterns: refutable and irrefutable. They each have their own grammar and the same syntax in one can mean something different in the other. In particular, identifiers are treated as constants in refutable patterns and variable binders in irrefutable ones.

The proposal adds irrefutable pattern-based variable declarations for destructuring like:

var (a, b) = ("a", "tuple");

If we were to use refutable patterns in variable declarations that have else at the end, then it means very similar declarations can be mean very different things:

var [a] = [123]; // Destructure 123 from the list and bind to "a".
var [a] = [123] else return; // Return if 123 is not equal to the constant "a".

I think that's probably a bad thing.

I really like taking Swift's approach to patterns because it works well in a lot of common cases, and makes it possible to have patterns that check for equivalence to named constants, which is a very common thing to do in switches today. But it means we have to be careful to telegraph to users which kind of pattern is expected in any given construct that builds on patterns.

Having two very similar flavors of variable declaration where one uses refutable patterns and the other irrefutable will confuse users. That's why I've suggested using case for refutable patterns, because that keyword already signals "refutable pattern ahead" in switch cases.

if (int x =~ someExpression) {
}
int x =~ someExpression else {
  // This block must exit
  return "not an int";
}

We could do that, but I think I would expect that to mean a different kind of equality method being called (maybe for stuff like regex match) and not an entirely different syntactic operation.

Or re-use instance checks?

if (someExpression is int x) {
}

someExpression is int x else {
  // This block must exit
  return "not an int";
}

Maybe, but that probably doesn't generalize to nested patterns and destructuring well.

@leafpetersen
Copy link
Member Author

If we were to use refutable patterns in variable declarations that have else at the end, then it means very similar declarations can be mean very different things:

var [a] = [123]; // Destructure 123 from the list and bind to "a".
var [a] = [123] else return; // Return if 123 is not equal to the constant "a".

Why is this specific to else in your mind? All of these proposals re-use the refutable patterns in variable declarations. In your other proposal:

var [a] = [123]; // Destructure 123 from the list and bind to "a"
unless (case var [a] = [123]) {
  // Return if 123 is not equal to the constant "a".
}

I agree that the ambiguity is confusing, but it doesn't feel substantially less so just because I put the keyword(s) before instead of after.

@munificent
Copy link
Member

Why is this specific to else in your mind?

Because that form looks a lot more like a variable declaration to me than a control flow construct.

All of these proposals re-use the refutable patterns in variable declarations. In your other proposal:

var [a] = [123]; // Destructure 123 from the list and bind to "a"
unless (case var [a] = [123]) {
  // Return if 123 is not equal to the constant "a".
}

I agree that the ambiguity is confusing, but it doesn't feel substantially less so just because I put the keyword(s) before instead of after.

It does to me. In the syntax here, there is a case keyword that clearly suggests "the thing after case is the same kind of thing as after case in a switch". This is something that might be worth doing a user study for once we have a proposal that's settled down some.

@lrhn
Copy link
Member

lrhn commented Jan 26, 2022

If we were to have a guarding let, I'd like it to be an expression. (Not just because I like expression level bindings, but also because it forces the null-result to be visible in the typing.)

Say we have let id = expr in expr as an expression. Or (var id = expr; expr). Strawman syntax, what matters is that it's an expression-level let.

Then we could define let id ?= expr in expr as the null-aware binding which ensures id is non-null, and skips the in part if the value is null (and then everything is null).
Could even add an else branch, let id ?= expr in expr else expr too.

Or maybe just let let id ?= expr be a boolean expression evaluating to true if assignment happens and false if not, and if used in a conditional position, the scope extends to the true branch. (But that's probably contentious, and why we are talking about a special branching let language construct instead of allowing the scope of a condition to extend to the branches of a normal if statement.)

@leafpetersen
Copy link
Member Author

leafpetersen commented Jan 26, 2022

If we were to have a guarding let, I'd like it to be an expression. (Not just because I like expression level bindings, but also because it forces the null-result to be visible in the typing.)

This doesn't really seem to be solving the same problem to me. Here's some code:

class C {
  Iterable<String>? maybeStuff;
  int test() {
    var stuff = maybeStuff else (stuff == null) { 
       return -1; 
    }
    for(var s in stuff) {
       var contents = readFile(s);
       // more stuff here
    }
    return 0;
  }
}

How does an expression level let help with this? And what problem with the above code does making the null result (I'm not sure what that means) explicit in the typing help with?

I'm fine with an expression level let - I find them very useful myself, but it still feels orthogonal to me.

@lrhn
Copy link
Member

lrhn commented Jan 27, 2022

It's true that if we want the scope to continue after the test, and we do not want expressions to introduce variables that survive the expression, then we need this to be a statement. (I am known to disagree on the latter condition.)

Am I right that

(var x = s) if (x != null) {
   // Block executed if condition is true, with promotion
}
// and 
// y is in scope in the condition and the continuation, block must exit
(var y = s) else (y == null) { 
   return;
}

are equivalent to

var x = s;
if (x != null) {
   // Block executed if condition is true, with promotion
}
// and 
var y = s;
// y is in scope in the condition and the continuation
if (y == null) {
  return;
}

The new syntax buys noting except moving the declaration onto the same line as the test, but at the cost of not having an explicit if (which I personally think is a big setback for readability, I honestly can't guess what (var y = s) else (y == null) { return; } means from the syntax.)

That's why I prefer if (var y = s; y == null) { return; } where the variable can escape the if statement if either branch definitely doesn't complete normally. The if is explicit, and just like any other if. I can clearly see the branches, nothing is implicit. It just moves the variable declaration closer to the first use.

It's exactly the same operation, the only difference is that the first keyword is if instead of var.
It differs from for(var i ...) in that the i variable is visible past the declaring statement (but I'd actually be delighted if I could access the variable of an if(var i = 0; i < n.length; i++) { if (something(n[i])) break; } after the loop, so maybe that's what we should change instead.)

(Also, using else here could preclude making if an expression, which some people have asked for).

@munificent
Copy link
Member

If we were to have a guarding let, I'd like it to be an expression. (Not just because I like expression level bindings, but also because it forces the null-result to be visible in the typing.)

For what it's worth, expression-level variable bindings feel very weird to me in Dart. Dart is not an expression based language. It's got statements, for better or worse. If we want to lift variable binding out of statements and into expressions, then it really feels to me like we should convert everything to be expression based including if, for, etc. A language that has expression level declarations but not expression level scopes or control flow feels like a chimera to me.

@leafpetersen
Copy link
Member Author

leafpetersen commented Jan 27, 2022

The new syntax buys noting except moving the declaration onto the same line as the test, but at the cost of not having an explicit if (which I personally think is a big setback for readability, I honestly can't guess what (var y = s) else (y == null) { return; } means from the syntax.)

That's why I prefer if (var y = s; y == null) { return; } where the variable can escape the if statement if either branch definitely doesn't complete normally. The if is explicit, and just like any other if. I can clearly see the branches, nothing is implicit. It just moves the variable declaration closer to the first use.

The critique in your first paragraph applies equally to your second paragraph no? I'm somewhat in agreement with the critique taken broadly (applying both to this proposal and to your second paragraph), but as a counterpoint, we've heard from a number of programmers coming from Swift that they miss guard let quite a bit, and from various folks on these issues that splitting apart the constructs into a block level declaration + a normal if is unappealing.

@Levi-Lesches
Copy link

Levi-Lesches commented Jan 27, 2022

I think the point is that if it's just moving the declaration onto the same line, it ought to help with readability. After all, you can always just write:

var y = s; if (y == null) { return; }

I suppose that's probably why in #1201, there was a lot of support for if-let, but some pushback against guard-let, which messes with scoping. I feel that just sacrificing a line to the declaration is often more intuitive. I'll concede that I've seen some intuitive examples of the guard-let form, but for the most part, I think the proposal still lacks a clear way to indicate "this is an if statement, and the variable declared here is visible outside of that. Don't let the fact that it's trapped in parenthesis confuse you". Even my code snippet above gets that point across clearly, and you can read it as "here is a declaration, and here is a separate if statement that can help with promotion". I don't see any confusion with this:

class A {
  int? x;
  
  void local() {
    // regular promotion happens after this
    var y = x; if (y == null) return;
    print("Is not null");
    print(1 + y);  // perfectly fine
  }
}

Actually, I'm starting to like this more than if (var y = s; y == null) { return; }. It's exactly the same amount of characters but already works today, doesn't introduce new scoping rules, and users can bring it back to two lines whenever they want.

@lrhn
Copy link
Member

lrhn commented Jan 28, 2022

@munificent Dart's scope is already chimera-like in that it pretends to declare variables in statements, but really all variables are block scoped, not statement scoped. If they are declared somewhere later inside the block, their scope is still the entire block, you just can't refer to the variable until after the declaration.

var x = 0;
{
  print(x); // Invalid, `x` (the one below) is not declared yet.
  var x = 2;
}

If we embrace that design, which we have so far, I see no inherent problem with declaring variables inside expressions. The variables can still be block scoped, and still cannot be referenced from code not dominated by the declaration (which is determined by the exact same flow analysis that we already use to detect definite assignment).

It might be a little more complicated for users to decide whether a variable can be accessed or not, but I don't actually think it's a problem in practice. People will tend to follow specific patterns which are known to work, and the rest will likely understand the error message ("that variable declaration may not have been executed before reaching here.").

On the other hand, an expression declaration would be extremely useful for some of our more specialized expression-level language features, like the initializer list, forwarding constructors, collection literals and selector chains. Those are places where Dart already allows people to make large and complicated expressions, but does not provide a good way to do abstraction or reusing values.

@leafpetersen
Copy link
Member Author

If they are declared somewhere later inside the block, their scope is still the entire block, you just can't refer to the variable until after the declaration.

I understand that you like to think of that way but, 1) it is not the only way to think about it, and 2) if you ask users whether the second declared x is in scope in the print statement, I am highly confident that the vast majority will say "no". Further to 1), an equally valid perspective (and one that I prefer) is that the scope of the second x starts at its declaration, but that Dart chooses to make it an error to reference a variable which is in scope, but for which there is another variable with the same name declared at a later point in the same block. This is, I claim, much closer to how users think about it.

If we embrace that design, which we have so far, I see no inherent problem with declaring variables inside expressions. The variables can still be block scoped, and still cannot be referenced from code not dominated by the declaration (which is determined by the exact same flow analysis that we already use to detect definite assignment).

Why do you propose to make expression level variables block scoped and not expression scoped? Note that doing so deeply breaks your analogy to existing variable declarations. That is, the scope of block scoped variables end at the termination of the block. If you want an analogous construct for expressions, you should make the scope end at the termination of the expression (this is how standard let expressions work). If you do not do so, then you need to explain to me why you would not do the same for blocks, that is:

{ 
  { var x = 3; }
  print(x); // Still in scope, since it is dominated by the declaration?
}

The reason, of course, is that 1) it is useful for people to think of scopes via a syntactic hierarchy, rather than through dominance, as anyone who has spent a lot of time debugging SSA code can tell you... :), and 2) that there is value in a variable going out of scope. The fact that scopes are well-delimited is what allows you to avoid having to make all of your variable names globally unique, and that is valuable. And once you see this, then it should be completely clear that making variable level declarations be block scoped is a bad idea, because it extends the scope outside of the syntactic hierarchy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
field-promotion Issues related to addressing the lack of field promotion
Projects
None yet
Development

No branches or pull requests

5 participants