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

Discussion: let statement #9876

Closed
alrz opened this issue Mar 18, 2016 · 13 comments
Closed

Discussion: let statement #9876

alrz opened this issue Mar 18, 2016 · 13 comments

Comments

@alrz
Copy link
Contributor

alrz commented Mar 18, 2016

Since #6400 was closed, I wanted to discuss some other aspects of let statement and potential improvements.

Variable shadowing

A relaxed form of shadowing were discussed at #8016 which didn't seem to be a desired behavior. I want to propose a restricted form of shadowing for let that allows to use same variable name in the pattern as the target,

let foo = foo as Bar;

This allows us to use the same name wherever it makes sense, e.g.

foreach(var item in list) {
  let Some(var item) = item else continue;
}

Multiple variables and patterns

Since #4294 is closed, I think it'd be nice to be able to declare multiple variables with let,

let-statement:
simple-let
complex-let

simple-let:
let local-variable-declarators ;
let complex-pattern = expression ;

complex-let:
let complex-pattern = expression when-clauseopt else-clause
let complex-pattern = expression when-clauseopt complex-let

For example,

let a = 5, b = 6;

It helps to use a single else for all fallible patterns,

let Point(var x, 0) = point1 when ...
let Point(0, var y) = point2 when ...
else throw new Exception();

// instead of
let Point(var x, 0) = point1 else throw new Exception();
let Point(0, var y) = point2 else throw new Exception();

which prevents code duplication.

Null check

As it is specified, the simplified form of let statement always succeeds, which would make else part unnecessary. I want to suggest to allow else for all nullable expressions, so,

let bar = foo as Bar else return;
// instead of
let Bar bar = foo else return;

As an alternative to the followings,

var bar = foo as Bar;
if(bar != null) { ... }

if(foo is Bar bar) { ... }

which introduce a level of additional indention that let is trying to avoid in the first place.

Identifiers as variables

Currently var is being used to disambiguate identifiers and new variables in patterns,

let (var x, var y) = (1, 2);

Considering that this will be very common I want to suggest that let turns every identifier to a new variables in patterns, for example.

let (x, y) = (1, 2);

The syntax can be used in switch statements (Swift-like),

switch(...) {
  case (var x, var y):
  // equivalent to
  case let (x, y): 
}

Same analogy applies to let statement and case expression,

let (x, y) = (1, 2);
// equivalent to 
let case (var x, var y) = (1, 2);

var result = tuple case let (x, y): x + y;
// equivalent to
var result = tuple case (var x, var y): x + y;

As patterns

We can use let as as-pattern instead of an identifier after pattern, e.g.

switch(expr) {
  case let tuple = (var a, var b):
}

// instead of
switch(expr) {
  case (var a, var b) tuple:
}

So I propose replace var patterns with this production rule:

simple-pattern:
constant-pattern
wildcard-pattern
let-pattern

let-pattern:
let identifier
let identifier = complex-pattern
let complex-pattern

let expressions

It would be nice to allow decomposition assignment for complete patterns as an expression (#254).

One use case is in using statements so that one be able to write this:

using ( let Foo(var x) = Bar() ) {}

In this example, let returns a Foo which is IDisposable and variable x is scoped inside using.

@HaloFour
Copy link

I don't like the idea of introducing local shadowing, using any syntax. I don't see why let wouldn't fall under the same identifier rules as anything else in the language.

You aren't really talking about multiple variables, you're talking about multiple patterns. From what I understand let will permit multiple variable patterns as subpatterns, so you could do the following:

let (Point(var x, 0), Point(0, var y)) = (point1, point2) else throw new Exception();

As for the final point, I don't see much benefit for having the alternate as syntax there. It's more familiar, but it's also more verbose.

@alrz
Copy link
Contributor Author

alrz commented Mar 18, 2016

I don't see why let wouldn't fall under the same identifier rules as anything else in the language.

Because it might introduce new variables in the pattern, and they potentially make the target useless. For example in foreach at best you need to introduce a new name even though you don't need the first one (actually I didn't come up with a reasonable name for this particular case).

let (Point(var x, 0), Point(0, var y)) = (point1, point2) else throw new Exception();

Even if the intermediate tuple optimizes away, the readability is far from my example's. It gets worse when you want to use when.

It's more familiar, but it's also more verbose.

There is no new syntax there, just a relaxed rule, because patterns are meant to check for null anyway. Actually it might be confusing that let bar = foo as Bar is a complete pattern.

As for the final point

There is more.

@HaloFour
Copy link

Because it might introduces new variables in the pattern,

I understand what you're trying to do, that's generally why anyone would want to shadow. But I think it's a slippery slope and I don't think that it's too onerous to have to assign the pattern results to new identifiers even if you don't plan on using the old one again within that scope.

Even if the intermediate tuple optimizes away, the readability is far from my example. It gets worse when you want to use when.

I think it's more explicit about what is actually happening, though, whereas you're looking for special syntax to handle multiple patterns within a single deconstruction.

because patterns are meant to check for null anyway.

Variable patterns aren't. That's even true in F#. If you want the null check you should use the type pattern.

There is more.

Pretty sure that wasn't there when I was typing my comment. 😄 I'm pretty sure that requiring var for variable patterns was an explicit decision, and that allowing it to be omitted specifically in the case of being the only pattern in a let statement was only because it is otherwise quite redundant. Maybe the argument could be made that when using let for simple deconstruction of tuples that the var isn't necessary but that creates an inconsistency with how those patterns work anywhere else. And I don't think that extending it beyond that case makes any sense. Typos shouldn't result in new variables.

Note that I'm largely playing Devil's advocate about a lot of this. I don't have that strong of an opinion one way or another.

@alrz
Copy link
Contributor Author

alrz commented Mar 18, 2016

that's generally why anyone would want to shadow.

As it turns out, no. For example, in #8016 it is proposed that one should be able to shadow locals just for the sake of it and no other reason, really. Since, as I said, it would be not possible to do that and you can just use the target name (if it was an identifier and nothing else), nothing can possibly go wrong.

I think it's more explicit about what is actually happening, though,

It's more explicit but it's like you're inlining everything. In this specific example, you don't want to use a single when but using a tuple you have to, even though that's just the case for the else part. Besides that I don't really like to create tuples and deconstruct them right away (the compiler has feelings) it just explodes and there is no way to cleanly format the code. And it's not just that, since let is not a replacement for a type like var, for simple variable declarations, you don't need a Matrix to make it sensible.

If you want the null check you should use the type pattern.

Yes, I meant in case of a type pattern which you would normally use as and a null check. It just reads a lot better that way. I expect that an analyzer would turn that pattern to if(o is T t) which then you have an additional level of intention that let is trying to avoid in the first place. So you need to change your coding style to the old T t = e and mention the type before deciding on anything else.

I'm pretty sure that requiring var for variable patterns was an explicit decision, and that allowing it to be omitted specifically in the case of being the only pattern in a let statement was only because it is otherwise quite redundant.

Yes that is, because case identifier is currently valid C# syntax. My suggestion does not invalidate it. Also, I think let is supposed to used for declaring variables (via patterns) right? So why when I write let, still I need to write var? It just seem redundant. And yes this is the reason that it is special cased in the obvious case!

let for simple deconstruction of tuples that the var isn't necessary but that creates an inconsistency with how those patterns work anywhere else. And I don't think that extending it beyond that case makes any sense.

Does this make any sense?

let Point(x, y) = point;

Typos shouldn't result in new variables.

That is like saying typos in var also result in new variables! That is supposed to result in a new variable, right? This syntax does not make any other cases impossible, you still have let case, case, and case let which is used for the exact same purpose in Swift.

@alrz
Copy link
Contributor Author

alrz commented Mar 24, 2016

I've updated the openning post to mention let as as-patterns, though it is more verbose, but I think it would be more readable when the pattern is rather complex.

@leppie
Copy link
Contributor

leppie commented Mar 24, 2016

Please expand let to accept a method body too. IOW:

let f = 
{
   blah;
   foo;
   return bar;
};

which is semantically the same as:

var f = new Action(() => 
{
   blah;
   foo;
   return bar;
}).Invoke();

This is compatible with expression 'bodies' too.

Obviously the compiler should optimize the closure and call out, but keep scoping intact.

Another alternative would be:

var f = let { body };

// possible usage
new 
{
   something,
   bar = let 
   {
      var x = blah.expensive().baz;
      return new {x.a, x.b };
   }
};
// instead of
new 
{
   something,
   bar = new 
   {
      blah.expensive().baz.a, 
      blah.expensive().baz.b 
   }
};
// or creating (read scoping) the variable before

@HaloFour
Copy link

@leppie

It sounds like you want some kind of new syntax to allow for inline declaration and invocation of a local function. I don't see what that would have to do with let which is for deconstruction.

@alrz
Copy link
Contributor Author

alrz commented Mar 24, 2016

@leppie #6182

@leppie
Copy link
Contributor

leppie commented Mar 24, 2016

@HaloFour This would never be function, I just used it to describe the semantics, but as @alrz pointed out, such a suggestion already exists. My bad :D

@HaloFour
Copy link

Ah, if the point is to describe a sequence of operations that are to be treated as an expression, then yes, #6182 appears to be the right feature. And it should work with let just as it would with anything else that accepts an expression.

@leppie
Copy link
Contributor

leppie commented Mar 24, 2016

@alrz Can you make it clear this is primarily used for deconstruction (somewhere near the top)? :)

@alrz
Copy link
Contributor Author

alrz commented Mar 24, 2016

@leppie I'm not quite sure what you're asking but let is all about deconstruction as originally proposed in #6400 + a special case for declaring read only locals.

@alrz
Copy link
Contributor Author

alrz commented Jan 19, 2017

Obsolete.

@alrz alrz closed this as completed Jan 19, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants