Skip to content

[Proposal] Declaration Expressions #595

mattwar asked this question in General
[Proposal] Declaration Expressions #595
3y ago · 47 answers

@mattwar mattwar 3y ago
Collaborator

Now that 7.0 is out and C# has introduced out variables and type patterns (with variable declaration), its time to consider a general purpose declaration expression that can introduce variables within expressions.

A declaration expression is fundamentally an initialized variable declaration as an expression instead of a statement.

Basic Declaration Expression

declaration-expression:  <type> <name> = <expression>

But you'll probably need parentheses to disambiguate within most expressions. The value of the declaration expression is the value of variable after it is declared. This is basically the same as an assignment expression, except you are declaring the variable too.

Today you can write code like this with assignment expressions, and in certain code bases it may be a familiar pattern.

char ch;
while ((ch = GetNextChar()) == 'a' || ch == 'b' || ch == 'c') 
{
}

This is okay, but requires you to declare the variable in the outer statement context, and while that doesn't appear to be too onerous given the example, its certainly possible that such a statement scope might not even be available, such as in a field initializer, or base class invocation expression.

With a declaration expression you would be able to also declare the variable within the expression. It would have the same visibility as a variable declared by an out variable or an is pattern.

while ((char ch = GetNextChar()) == 'a' || ch == 'b' || ch == 'c') 
{
}

Extended Declaration Expressions

When you have an is pattern variable used in an expression you get to refer to the variable within the rest of expression because the is expression is a boolean expression and you have a natural way to extend boolean expressions with the '&&' and '||' operators.

if (o is string s && s.StartsWith("flurp")) 
{  
}

Out variables used in expressions are typically the same because they are often used in methods following the Try pattern, so they too return bool and you typically get to refer to these declared variables as a part of a continuation of that boolean expression.

In the example for the declaration expression above, the declared variable was also used later in the expression because the overall expression was turned into a boolean expression by using '=='.

while ((char ch = GetNextChar()) == 'a' || ch == 'b' || ch == 'c') 
{
}

But that might not be the only reason to need a variable assignment within an expression. For example, I may want to calculate a value and then use parts of it in a follow on expression. In order to do this we need the ability to compute a follow on expression where the declared value is visible where we don't currently have an operator to do that.

For example, if I had a statement context, I might want to write code that fetches a value or instance from one method and use that value or instance more than once in a separate expression.

var p = GetPoint();
var result = p.X + p.Y;

But this would be impossible to write as a single expression, unless we could have another expression follow on from the first using some other operator.

Introducing an extended declaration expression.

<extended-declaration-expression> = (<declaration-expression> ; <expression>)

An extended declaration expression follows the declaration with a semicolon (or to be determined better punctuation) and another expression that can be evaluated with references to the declared variable. The value of the extended declaration expression is the value of the follow on expression.

Now you can write:

   (var p = GetPoint(); p.X + p.Y)

If you need to.


Updated example to use arbitrary type with members and addition instead of invocation to avoid confusion with a potential tuple splat operator.

Replies

47 comments

THe use cases are very uncompelling to me :)

The last one, especially, feels particularly like the wrong solution to the problem as stated. When i see someone say they had to write:

var (x,y) = GetMultipleReturnValues();
var result = DoSomethingWithThoseValues(x, y);

And they'd like it as a single expression, i'd want that solution to be:

var result = DoSomethingWithThoseValues(GetMultipleReturnValues());  // or maybe
var result = DoSomethingWithThoseValues(GetMultipleReturnValues()...);  // ... to indicate splatting

(perhaps with some minor ceremony), and definitely not:

  (var (x,y) = GetMultipleReturnValues(); DoSomethingWithThoseValue(x, y))

:)

0 replies

That said, let's not dive into the weeds on splatting. I don't want to derail. I just want to point out that if there are to be examples, i want them to resonate with me. :)

0 replies

@mattwar mattwar 3y ago
Collaborator Author

Sure, maybe I need some more compelling example. Splatting might work for the explicit example I used, but not generally.

0 replies

This is a good example of how as patterns could be used:

while ((GetNextChar() as var ch) == 'a' || ch == 'b' || ch == 'c') 
{
    // use ch here
}
0 replies

@mattwar mattwar 3y ago
Collaborator Author

To avoid splatting comparison, instead I get multiple values from one source and then operate on them in an non-spat way, ... like addition.

  (var p = GetPoint(); p.X + p.Y)
0 replies

@mattwar mattwar 3y ago
Collaborator Author

@DavidArno as patterns may have problems working with value types. But it is similar.

0 replies

@mattwar your last example looks suspiciously like a sequence expression (#72)

0 replies

@mattwar mattwar 3y ago
Collaborator Author

@orthoxerox yes, but with only a very limited sequence.

0 replies

@mattwar mattwar 3y ago
Collaborator Author

After thinking about it, there is a way to do this already.

   (GetPoint() is var p ? p.X + p.Y : default(int))

This works, but is a bit ugly, since I have to use the ternary and have to supply the false case that never executes.

0 replies

@DavidArno afaik that's not an as pattern, it's useful to name the whole pattern, like x is ((1, var a) as t)

0 replies

@alrz,
Sure, it's not a pattern. I just didn't have a better name for it 😀

@mattwar,
as as it stands doesn't handle value types. But, as per my set of suggested rules on "as patterns", x as var y could handle value types just fine (I think).

0 replies

@alrz,

Just realised what you did there. A proper "as pattern" could be really useful in pattern matching. For HandleTuple((int x, int y) t) => ..., the following would be possible with that pattern:

switch (x)
{
    case (1, var a) as t when MeetsCondition(a): 
        HandleTuple(t);
        break;
    ...
}
0 replies

while ((char ch = GetNextChar()) == 'a' || ch == 'b' || ch == 'c')

for that one, I'd prefer an "or" pattern (#118),

while (GetNextChar() is ('a' | 'b' | 'c'))

or something like that.

(var (x,y) = GetMultipleReturnValues(); DoSomethingWithThoseValue(x, y))

As an alternative to splatting or sequence expressions for this case, the "case expression" might be interesting too (present in the long-outdated pattern spec),

var result = GetMultipleReturnValues() case var (x, y): DoSomethingWithThoseValue(x, y);

However, splatting is the obvious solution for this specific example.

0 replies

@DavidArno Yeah, this is a little off-topic though but you probably want to scratch as as the chosen token for that since it has the same precedence as is so e is p as t is already a valid syntax.

0 replies

@mattwar mattwar 3y ago
Collaborator Author

I updated the example for extended declaration expression to not use tuple and then immediate invocation to avoid confusion with a possible tuple splat operator.

0 replies

@mattwar mattwar 3y ago
Collaborator Author

Following on with the idea of substitution

If I had the functions:

    int F(int a, int b);
    int G(int c);

and wanted to compose an expression that did this:

   var p = GetPoint();
   var r = F(G(p.X), p.Y);

I could write it as:

   F(G((var p = GetPoint()).X), p.Y)

because order of evaluation works in my favor.

but if I wanted to write:

   var p = GetPoint();
   var r = F(p.X, G(p.Y));

I would be in a bit of trouble.

I guess I cannot always rely on substitution! That means extended declaration expressions are back on the menu!

0 replies

@mattwar mattwar 3y ago
Collaborator Author

Nope. I'm wrong.. (sigh)

   F((var p = GetPoint()).X, G(p.Y))
0 replies

@mattwar mattwar 3y ago
Collaborator Author

Apparently, I can have this discussions all by my self.

0 replies

Don't worry, we will be your silent rubber ducks.

0 replies

@mattwar mattwar 3y ago
Collaborator Author

I'm good at contradicting myself and arguing against what I just argued for. Makes it more exciting.

0 replies

Could be (var p = GetPoint(); F(G(p.X), p.Y)) instead and it also works for the latter case.

I think it would make sense to restrict declaration expressions to not appear everywhere.

0 replies

@mattwar mattwar 3y ago
Collaborator Author

@alrz I agree that it the extended declaration expression works and probably is more clear. However, I was going through the process of thinking through the alternative because my original argument for the extended declaration expression claimed that it was not possible to express it any other way, but I disproved that. It is possible using the basic declaration expression alone and inlining it in any expression, or even using the is operator and the ternary. But I'd still rather not use the alternatives.

0 replies

@jmagaram

Or even more concisely:

    using static LanguageExt.Prelude;

    var result = from x in Some(CalculateX())
                 where x % 5 == 0
                 from y in Some(CalculateY())
                 where y % 3 == 0
                 select x + y;

But to the point of this discussion. F# allows declarations of variables (values) in expressions, because this:

    let x = 10
    let y = 20
    x + y

Can be translated to C# as:

    ((int x) =>
        ((int y) =>
            x + y)(20))(10);

That won't compile, but you get the idea. So I think if we accept that an inline declaration creates a scope over the rest of the expression which doesn't abuse the sanctity of the expression, then this would solve an enormous number of problems with trying to write code in an expression oriented style in C#.

May I suggest that let would be better syntax to use? As it already is used in LINQ and makes sense if it's considered a scoped lambda, because it creates a scope in LINQ too.

I think this only works inline if you consider the expression like the lambda above. So:

    (int x, int y) MovePoint(int amount) =>
        let (x, y) = GetPoint(),
        (x + amount, y + amount);

That allows expressions to pre-load variables so they don't get fetched multiple times like so:

    (int x, int y) MovePoint(int amount) =>
        (GetPoint().x + amount, GetPoint().y + amount);

This ^^ is definitely one of the big pain points for me with expression oriented coding.

Things like this (from the original proposal) though I think are hard to parse and understand:

    char ch;
    while ((ch = GetNextChar()) == 'a' || ch == 'b' || ch == 'c') 
    {
    }

In that situation you'd want:

    while ( let ch = GetNextChar(), ch == 'a' || ch == 'b' || ch == 'c' ) 
    {
    }

I'm using a comma for a separator. But I'm not particularly attached to it.

0 replies

10 years later... Microsoft writes entire .net standard 5 library in one statement

0 replies

Just the other day I wanted to do a let binding in an expression to partially eval a calculation

public static CurriedFunction F =
 (int x) =>
   let y = expensive(x)
   z => notSoExpensive(y,z)

so I also, like @louthy above, would like let-expressions out side linq

0 replies

Lambda's allow for full statement bodies unless you are really using Expression.

public static CurriedFunction F =
    (int x) => {
      var y = expensive(x);
      return z => notSoExpensive(y, z);
    }

And if you are writing a method body, just use a full body:

(int x, int y) MovePoint(int amount) {
    var (x, y) = GetPoint();
    return (x + amount, y + amount);
}

But I think there are other cases where declaration expressions are helpful.

0 replies

Can we also include here the following use case?

void MyEventHandler(object sender, MyEventHandlerArgs args)
{
...
}

object SomethingThatTriggersEvent()
{
  MyEventHandler(sender, var args=new MyEventHandlerArgs(){ InputParam1=123});
  return args.OutputParam1;
}
0 replies

Honestly, I don't like it. Mixing assignment and reading of the same variable is already messy enough. Adding declarations to the mix makes it even harder to read and the examples I've seen are not very compelling.

It made sense with pattern matching and LINQ because they would be unusable otherwise. But to open the floodgate by allowing declarations to literally appear anywhere is too much.

0 replies

A couple of extension methods allow use of the existing out var pattern to simulate Let/As:

public static T Let<T>(T val, out T newvar) => newvar = val;
public static T As<T>(this T val, out T newvar) => newvar = val;

For Let, I recommend a using static on its containing class.

Then you can have

while (Let(GetNextChar(), out char ch) == 'a' || ch == 'b' || ch == 'c')  { }
Let(GetPoint(), out var p).X + p.Y)

Or with As

while (GetNextChar().As(out var ch) == 'a' || ch == 'b' || ch == 'c')  { }
var r = GetPoint().As(out var p).X + p.Y;

Though it does require use of the out keyword and I don't suppose the optimizer will take out the method call.

Having declarations be expressions would allow something like

public static string PastLast(this string s, string starter) {
    var starterPos = s.LastIndexOf(starter);
    return starterPos == -1 ? String.Empty : s.Substring(starterPos + starter.Length);
}

be reduced to

public static string PastLast(this string s, string starter) =>
    (var starterPos = s.LastIndexOf(starter)) == -1 ? String.Empty : s.Substring(starterPos+starter.Length);

or possibly

public static string PastLast(this string s, string starter) =>
    (s.LastIndexOf(starter) as starterPos) == -1 ? String.Empty : s.Substring(starterPos+starter.Length);
0 replies

I have often suggested overloading double-colon for this (value::newVarName):

    if (validating && GetPerson(form.FullName)::person.Age >= 18) {
        if (!form.HasSignatureFrom(person))
            errors.Write(person, "Persons 18 or older must sign the form.");
    }

The advantage of this syntax is that it often requires no additional parentheses.

A problem with allowing variable declarations inside expressions (as C# already does with out Foo<T> foo) is that declarations are not as easy to spot as they were before. I suggest introducing a different syntax coloring or style to highlight variable declarations, e.g. draw a box around variables at the declaration point, or add a drop-shadow. Though perhaps it would be even more informative to draw a box around (or otherwise change the style of) variables at all locations where they are assigned.

0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
#️⃣
General
Converted from issue
Beta