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

[Proposal] Declaration Expressions #595

Open
mattwar opened this Issue May 17, 2017 · 42 comments

Comments

Projects
None yet
@mattwar

mattwar commented May 17, 2017

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.

@mattwar mattwar added the Discussion label May 17, 2017

@mattwar mattwar self-assigned this May 17, 2017

@CyrusNajmabadi

This comment has been minimized.

CyrusNajmabadi commented May 17, 2017

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))

:)

@CyrusNajmabadi

This comment has been minimized.

CyrusNajmabadi commented May 17, 2017

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. :)

@mattwar

This comment has been minimized.

mattwar commented May 17, 2017

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

@DavidArno

This comment has been minimized.

DavidArno commented May 17, 2017

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
}
@mattwar

This comment has been minimized.

mattwar commented May 17, 2017

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)
@mattwar

This comment has been minimized.

mattwar commented May 17, 2017

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

@orthoxerox

This comment has been minimized.

orthoxerox commented May 17, 2017

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

@mattwar

This comment has been minimized.

mattwar commented May 17, 2017

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

@mattwar

This comment has been minimized.

mattwar commented May 17, 2017

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.

@alrz

This comment has been minimized.

Contributor

alrz commented May 17, 2017

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

@DavidArno

This comment has been minimized.

DavidArno commented May 17, 2017

@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).

@DavidArno

This comment has been minimized.

DavidArno commented May 17, 2017

@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;
    ...
}
@alrz

This comment has been minimized.

Contributor

alrz commented May 17, 2017

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.

@alrz

This comment has been minimized.

Contributor

alrz commented May 17, 2017

@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.

@mattwar

This comment has been minimized.

mattwar commented May 17, 2017

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.

@mattwar

This comment has been minimized.

mattwar commented May 17, 2017

Given appropriate linq pattern tricks, I can use linq query syntax.

  (from p in GetPoint() select p.X + p.Y)

But my logic has to end up in a delegate.

@DavidArno

This comment has been minimized.

DavidArno commented May 17, 2017

@alrz,

You are right, I have taken this thread off topic. So I'll take yours and @mattwar's ideas over to my own proposal. Thanks to you both for some great examples for me though.

@alrz

This comment has been minimized.

Contributor

alrz commented May 17, 2017

@mattwar

I'd be reluctant to abuse linq like that. The reason I prefer case expressions for this is that they would bring variable decl and usages closer together,

var result = GetValues() case var (x, y): x + y;

far better than var result = (var (x, y) = GetValues(); x + y);

@mattwar

This comment has been minimized.

mattwar commented May 17, 2017

@alrz I agree. I was trying to explore alternatives.

@YaakovDavis

This comment has been minimized.

YaakovDavis commented May 17, 2017

I'd rather see the first case supported by patterns as follows:

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

Is this planned/proposed?

Edit: Yup.

@mattwar

This comment has been minimized.

mattwar commented May 17, 2017

@YaakovDavis There is no formal proposal for the as operator pattern. But you can use the is operator that already exists.

while ((GetNextChar() is var ch && ch == 'a') || ch == 'b' || ch == 'c') 
{
}

You can do this, because the example is in a boolean expression context.

@jmagaram

This comment has been minimized.

jmagaram commented May 17, 2017

What a coincidence that this morning I was running into the exact problem this proposal tries to address. Sometimes I find the need to define a variable to hold the value of a calculation just so I can use it more than one time in a follow-up expression. I find this cumbersome and not very "functional". The best solution I could come up with was an extension method that applies to pretty much everything...

public static class ObjectExtensions
{
    public static U Select<T, U>(this T item, Func<T, U> selector) => selector(item);
}

Here is how it gets used...

public string[] CalculateErrors() => new string[] { "a", "b", "c" };

public (int UnitsSold, double PricePerUnit, double Tax) GetMostRecentSale() => (4, 5.6d, 0.08d);

public void X()
{
    // The old way; must introduce a temporary variable

    var errors = CalculateErrors();
    string summaryA = errors.Any() ? $"There are {errors.Count()} messages." : "no messages";

    var sale = GetMostRecentSale();
    double totalA = (sale.UnitsSold * sale.PricePerUnit) * (1.0d + sale.Tax);

    // With the extension method can express it more succinctly

    string summaryB = CalculateErrors().Select(i => i.Any() ? $"There are {i.Count()} messages." : "no messages");

    double totalB = GetMostRecentSale().Select(i => (i.UnitsSold * i.PricePerUnit) * (1.0d + i.Tax));
}

I think, but am not sure, that this is supported in F# with the match statement that works on any type of variable. It would be nice if there was some built-in way for C# to do it. I feel like I'm kind of breaking the rules defining that all-encompassing extension method.

@quinmars

This comment has been minimized.

quinmars commented May 18, 2017

@mattwar

The first example could be written as:

public IEnumerable<char> GetChars()
{
    yield return GetNextChar();
}

// ...

foreach (var ch in GetChars().TakeWhile(c => c == 'a' || c == 'b' || c == 'c'))
{
    // Do something
}

Personally, I find that much more readable.

@YaakovDavis

This comment has been minimized.

YaakovDavis commented May 18, 2017

@quinmars
The snippet above changes the performance characteristics of the loop though due to allocations, which could be significant in parsing contexts.

@jnm2

This comment has been minimized.

Contributor

jnm2 commented May 18, 2017

@quinmars Sure, but I want declaration expressions just as much. I hate code where I have to do this:

if (cond)
{
    var x = expr1;
    if (x.cond)
    {
        var y = expr2;
        if (y.cond)
        {
            // ...
        }
    }
}
@alrz

This comment has been minimized.

Contributor

alrz commented May 18, 2017

Worth to mention, C++ added if and while initializers (just like what we have for for except that it's optional.

while (var x = e; condition) {}
if (var x = e; condition) {}

compared to sequence expressions, this saves a pair of parentheses but restricted in usage.

@jnm2

This comment has been minimized.

Contributor

jnm2 commented May 18, 2017

Ooh, I did not know that! I'll be able to stop using for (var x = e; condition;) {}. =)

@mattwar

This comment has been minimized.

mattwar commented May 18, 2017

Thinking about it some more, my reasoning for wanting extended declarations is faulty. I argue that while in boolean expression contexts where an is expression can be used to assign a variable and continue to use it, there is no continuation operator that allows you to do so outside of this one niche.

However, it is not actually necessary to have any such operator at all. Given a basic declaration expression as described above, I can write

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

as an expression like this

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

So, I can always just use substitution and rely on evaluation order to assure that my new variable is assigned.

And given that I can use a ternary to convert an is operator into a value expression

var r = ((GetPoint() is var p) ? p : default).X + p.Y;

I can substitute this ugly guy in its place, and look, no need for any new language feature.

of course, maybe this one is still clearer.

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

I don't know.

@jmagaram

This comment has been minimized.

jmagaram commented May 18, 2017

I find this var r = (var p = GetPoint()).X + p.Y; to be quite confusing. If all you are doing is transforming one value into another I think a Select or Map extension method (as described earlier) is quite intuitive.

var result = GetPoint().Map(p=>p.X+p.Y)

If you are trying to combine multiple values into one result, I think the LINQ-type syntax works nicely. Here's how you could do something similar using the library https://github.com/louthy/language-ext created by @louthy. This is a more complicated example, since you're only trying to add values when certain conditions are met.

Option<int> result =
    from x in Option<int>.Some(CalculateX()).Where(x => x % 5 == 0)
    from y in Option<int>.Some(CalculateY()).Where(y => y % 3 == 0)
    select x + y;

That is equivalent to a more verbose solution where you define additional variables to capture intermediate values.

int? CalculateResult()
{
    int x2 = CalculateX();
    if (x2 % 5 == 0)
    {
        int y2 = CalculateY();
        if (y2 % 3 == 0)
        {
            return x2 + y2;
        }
    }
    return new int?();
}
@alrz

This comment has been minimized.

Contributor

alrz commented May 18, 2017

extended declarations as proposed in #377 are subject to abuse while case expression (read single-arm match expression) gives a much nicer syntax var r = GetPoint() case var p: p.X + p.Y; . Unlike Map suggested by @jmagaram above, it could deconstruct into any pattern (including var) before getting into the RHS expression. It's equivalent to var r = GetPoint() is var p ? p.X + p.Y : default; without the redundant default (because var patterns always match).

@mattwar

This comment has been minimized.

mattwar commented May 18, 2017

@alrz your case expression still implies to the reader there is a type test going on, which is a confusing and unnecessary to add to the mix. It is true that the is operator combined with the var pattern does this too, but the existence of the var pattern is meant for use in recursive patterns that don't exist yet, and only was allowed in is expression for symmetry.

@mattwar

This comment has been minimized.

mattwar commented May 18, 2017

@jmagaram anything that requires a lambda is going to be too heavy-weight.

@mattwar

This comment has been minimized.

mattwar commented May 18, 2017

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!

@mattwar

This comment has been minimized.

mattwar commented May 18, 2017

Nope. I'm wrong.. (sigh)

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

This comment has been minimized.

mattwar commented May 18, 2017

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

@orthoxerox

This comment has been minimized.

orthoxerox commented May 18, 2017

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

@mattwar

This comment has been minimized.

mattwar commented May 18, 2017

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

@alrz

This comment has been minimized.

Contributor

alrz commented May 18, 2017

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.

@mattwar

This comment has been minimized.

mattwar commented May 18, 2017

@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.

@louthy

This comment has been minimized.

louthy commented May 19, 2017

@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.

@MkazemAkhgary

This comment has been minimized.

MkazemAkhgary commented Oct 5, 2017

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

@JohnNilsson

This comment has been minimized.

JohnNilsson commented Nov 13, 2018

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment