Proposal: Result Binding Expressions (to) #9822
Unanswered
AlexanderMorou
asked this question in
Language Ideas
Replies: 2 comments 7 replies
-
|
I prefer the let input = Console.ReadLine(),
fileBytes = File.ReadAllBytes(input),
sha = SHA1.Create().ComputeHash(fileBytes),
bitString = BitConverter.ToString(sha)
in Console.WriteLine(bitString);Since |
Beta Was this translation helpful? Give feedback.
2 replies
-
|
I feel like half of the point of pipe forward operator is to not name things that don't need to be named. This just brings the name right back, so it's objectively worse. |
Beta Was this translation helpful? Give feedback.
5 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Result Binding Expressions (
to)Summary
This proposal introduces result binding expressions for C#. A result binding expression uses the contextual keyword
toto evaluate an expression, bind its result to a new local variable (or discard it), and then evaluate a subsequent expression in the scope of that binding. The overall value of the result binding expression is the value of the second expression.Example:
In this case, the overall value is void so it yields no final value.
This feature is intended as a C#-native alternative to the pipe forward operator proposals (
|>/?>). It improves dataflow readability without introducing new symbolic operators or placeholder tokens, and it aligns with the language’s existing pattern of contextual keyword-based syntax and explicit variable bindings.Direct comparison with pipe proposals
The pipe forward operator proposal (#96) describes a chain of method calls like this:
Later comment I added suggested using a second operator for positional mechanics (
<|):With result binding expressions, the same flow becomes:
To get something similar to the Pipe-forward data-on-the-left, you would just reformat it:
For comparison, the equivalent code in current C# uses explicit locals:
The result binding expression form keeps the dataflow linear and explicit while staying
within the expression grammar.
Motivation
Multi-step transformations often become either deeply nested expressions or a series of explicit local variable declarations. Nested calls can be hard to scan and edit when you get more than a few levels deep. Pipe forward proposals attempt to alleviate this by providing a new operator to forward a value into a call, but after years of discussion they remain unchampioned. Issues include complex parameter placement rules and the need for additional placeholder syntax to achieve explicit control.
Recent C# evolution favors keyword-based constructs (patterns, query expressions, collection expressions) and expression-block features rather than new punctuation. Binding expressions follow this trajectory: they introduce a keyworded form that reuses existing variable semantics and composes with future sequence and expression blocks.
Relationship to existing pipe proposals
This proposal is an alternative to existing pipe forward proposals. Those proposals define new
|>,<|and?>operators and rely on implicit parameter placement rules. Binding expressions instead make the intermediate value explicit via a normal identifier, avoid adding new operators, and support non-call expressions and multiple uses of the bound value. If adopted, result binding expressions would cover the core motivations for pipe operators and render a separate pipeline operator unnecessary.Detailed design
Core syntax
A result binding expression has the form:
or, with type inference:
E1andE2are expressions.xis a local identifier. My preferred formatting is to placetoon the same line asE1and indent the subsequent expression, matching the pipeline shape. But the exact white space wouldn't influence the function. Each binding introduces a new scope forxand then flows into the next expression.Informal semantics
The result binding expression
behaves as if it were rewritten to a nested block (see champion #9243):
The exception to the champion #9243 is E2 may be a void expression, which has no value or data type (not even
null.)Evaluation proceeds as follows:
E1, including side effects.xand initialize it with the value ofE1. If an explicit typeTis specified,E1must be implicitly convertible toT. Ifvaror no type is specified,x's type is inferred exactly as forvar x = E1.E2in a scope wherexis in scope and definitely assigned.E2.When the binding designator is
_, special discard semantics apply. InE1 to _ E2;the underscore is treated as a discard.E1is evaluated and its result is ignored; no local variable is introduced and_cannot be referenced inE2. This special case also allowsE1to have typevoid, which is otherwise illegal to assign to a variable. Without the discard form, binding avoidexpression would produce a compile-time error.Chaining and pipelines
Result binding expressions compose naturally by chaining:
Conceptually this becomes nested blocks where each step introduces a new variable. Names are normal locals: they can be referenced multiple times in subsequent expressions, and nested bindings shadow outer names if needed.
Using the discard form
_allows side-effecting expressions that returnvoidto participate in a chain:Void-returning call in a pipeline
Console.WriteLinereturnsvoid. Without the discard form, binding the result would be illegal (CS8209); withto _the call is evaluated for its side effect and the pipeline continues.Expression vs statement
Binding expressions are expressions and can be used wherever an expression is
allowed. They are also valid as expression statements when the final
expression has a side effect:
Scope and definite assignment
The binding introduces a new local variable scoped only over the right-hand expression. Each nested binding creates a nested scope. The bound variable is definitely assigned at the start of the right-hand expression. Discards introduce no variable at all.
Typing rules
For
E1 to T x E2;:Sbe the type ofE1. There must be an implicit conversionS → T; otherwise a compile-time error occurs.E2,xis a local variable of typeTand is definitely assigned.E2.For
E1 to var x E2;andE1 to x E2;:x's type is inferred exactly as invar x = E1;. If inference fails, it fails here.E2.For
E1 to _ E2;:_is a discard. No variable is introduced and_cannot be used inE2.E1may have any type, includingvoid. The value ofE1is ignored.E2.Interaction with async and nullability
Binding expressions compose naturally with
awaitand with nullable analysis. For example:The rules for awaiting and nullable reference types are unchanged;
xis treated like any other local.Examples
Equivalent forms
Nested form:
Pipe operator proposal:
Binding expression:
Explicit locals:
Logging and metrics
The
to _binding discards the result ofConsole.WriteLine(...), allowing it to be sequenced before the remaining steps.Guarded transform
Tuples and anonymous types
Grammar changes (EBNF)
These grammar changes follow the style of the C# specification’s lexical and syntactic grammars.
Lexical grammar
Add
toas a contextual keyword:Expression grammar
Insert
binding_expressionas an alternative form ofnon_assignment_expression:null_coalescing_expressionensures that result binding expressions have lower precedence than??and higher precedence than assignment. The right-hand expression is a fullexpression, following the pattern of the conditional operator.Statement grammar
No change is required to the
expression_statementgrammar. Binding expressions may serve as statement expressions when their trailing expression is an invocation or other allowed statement expression.Drawbacks
to) and a new expression form, requiring parser, binder, and IDE support._discards the value ofE1and permitsE1to bevoid.Alternatives
|>,?>): these designs add new operators and rely on implicit parameter placement and optional placeholder tokens. They have not moved beyond discussion in several years.Unresolved questions
E1 to (int x, int y) coords E2)?Beta Was this translation helpful? Give feedback.
All reactions