Since #6400 was closed, I wanted to discuss some other aspects of let statement and potential improvements.
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;
}
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-clause_opt _else-clause_
let_complex-pattern_=_expression_ _when-clause_opt _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.
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.
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;
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.
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.
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.
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. :smile: 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.
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.
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.
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
@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.
@leppie #6182
@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
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.
@alrz Can you make it clear this is primarily used for deconstruction (somewhere near the top)? :)
@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.
Obsolete.
Most helpful comment
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
letjust as it would with anything else that accepts an expression.