Pipe-Forward Operator
- Dominant language
- No language data
- Stars
- 328
- Forks
- 71
- PR merge metrics
- No merged PRs in 30d
Description
This proposal addresses Scenario #154 and partially addresses Scenario #164.
# Summary
I propose adding a new operator to VB that passes its left operand as either the first argument or first operand of its right operand. This is similar to [F#'s pipe-forward operator](https://msdn.microsoft.com/en-us/visualfsharpdocs/conceptual/operators.%5b-h%5d-%5d%5b't1,'u%5d-function-%5bfsharp%5d) but different in that it passes its operand as the **first** argument rather than as the "last" as in F#. This difference both eliminates the requirement for function currying and is generally more applicable to how most .NET APIs are designed. In this way the `->` operator behaves similarly to an _aggregate function_ in a query `Into` clause.
``` antlr
PipeExpression
: Term LineTerminator? '->' LineTerminator? PipeTarget
;
PipeTarget
: Name ArgumentList
| ParenthesizedExpression '.' LineTerminator? SimpleName ArgumentList
| ( 'CType' | 'DirectCast' | 'TryCast' ) '(' LineTerminator? Type LineTerminator? ')'
| CastTarget '(' ')'
| 'If' '(' LineTerminator? Expression ',' LineTerminator? Expression ( ',' LineTerminator? Expression )? ')'
| 'Await'
| 'Not'
| ( '.' | '!' | '?' | '...' | '.!' )
;
ArgumentList
: '(' LineTerminator? Arguments? LineTerminator? ')'
;
CastTarget
: 'CBool' | 'CByte' | 'CChar' | 'CDate' | 'CDec' | 'CDbl' | 'CInt'
| 'CLng' | 'CObj' | 'CSByte' | 'CShort' | 'CSng' | 'CStr' | 'CUInt'
| 'CULng' | 'CUShort'
;
```
# Motivation
There are three primary benefits to this feature:
* Increase straight-forwardness by allowing lexical ordering to reflect execution order.
``` VB.NET
? (Await (Await obj.MAsync()).NAsync()).ToString().Trim().Length
' 5 3 1 2 4 6 7 8
' ^ Execution order ^
? obj.MAsync() -> Await ->
.NAsync() -> Await ->
.ToString().Trim().Length
' 1 2 3
' 4 5
' 6 7 8
' ^ Execution order ^
```
* Increase readability by allowing code to follow a natural Subject-Verb-Object order (when appropriate).
``` VB.NET
' From Roslyn source.
If Char.IsLetter(text(i)) Then
If text(i) -> Char.IsLetter() Then
If String.IsNullOrEmpty(args(0)) Then
If args(0) -> String.IsNullOrEmpty() Then
obj -> CallByName("Foo")
? str -> Trim()
For i = 0 To arr -> UBound()
? filename -> Path.GetExtension()
```
* Reduce the need for backtracking while typing code.
``` VB.NET
' From Roslyn source.
Dim accessor = TryCast(TryCast(getMethod.DeclaringSyntaxReferences(0).GetSyntax(cancellationToken),
AccessorStatementSyntax)?.Parent,
AccessorBlockSyntax)
Dim accessor = getMethod.DeclaringSyntaxReferences(0).GetSyntax(cancellationToken)
-> TryCast(AccessorStatementSyntax) -> ?.Parent
-> TryCast(AccessorBlockSyntax)
```
* Reduce the need for deeply nested invocations.
``` VB.NET
' From Roslyn source.
Return XElement.Parse(PdbValidation.GetPdbXml(compilation, qualifiedMethodName:=methodName))
Return compilation -> PdbValidation.GetPdbXml(qualifiedMethodName:=methodName) -> XElement.Parse()
' From Roslyn source.
ilImage = ImmutableArray.Create(File.ReadAllBytes(reference.Path))
ilImage = reference.Path -> File.ReadAlBytes() -> ImmutableArray.Create()
```
* Enable calling extension methods on `Object` with a fluent style:
``` VB.NET
For Each item In obj -> ReflectionHelpers.GetPublicProperties()
...
Next
```
* Enable calling lambdas (which can't be extension methods) with a fluent style:
``` VB.NET
Dim remove = Function(s As String, value As String) value.Replace(value, "")
? someString -> remove(badWords)
```
# Detailed Design
The design for the built-in operators is straight-forward. The design for method invocations requires more explanation.
## Precedence and Associativity
The big challenge with this feature is parsing precedence.
It's pretty obvious to which method obj is being passed in this example:
``` VB.NET
obj -> obj.A()
```
It's also obvious to which method obj is being passed in this example:
``` VB.NET
obj -> obj.A.B.C.D()
```
However, it's less immediately obvious to which method obj is being passed in this example:
``` VB.NET
obj -> obj.A().B.C.D()
' Is it this:
(obj -> obj.A()).B.C.D()
' Or this:
obj -> (obj.A().B.C.D())
```
And the default meaning is the later then potentially significant backtracking is now required to expression the former.
There's also an issue here of tooling; what completion options should appear and how should the final expression be formatted to indicate precedence.
To solve these questions this proposal gives `->` a higher precedence for argument lists than `.` and also requires an explicit `->` to transition back to normal associativity. This means if the target of the pipe is itself the result of a complex expression it's necessary to wrap that expression in parentheses:
``` VB.NET
' This isn't valid.
obj -> obj.A().B.C.D()
obj -> (obj.A().B.C).D()
```
This still requires parentheses but keeps them _after_ the `->` operator rather than requiring backtracking to before the left operand of the `->` and it's easier to go back into normal `.` precedence without any parentheses:
``` VB.NET
? obj.M() -> N() -> P() -> .ToString().Trim().Length
```
But consequently to pipe into a method invocation with an implicit receiver requires parentheses:
``` VB.NET
With someLongExpression
? node -> (.Foo.Bar).Baz()
End With
```
## Type-Inference and Overload Resolution
Overload resolution rules shouldn't change. It's an open question whether type-inference should work like extension method reduction where certain type arguments are fixed. This would give an experience consistent with extension methods but would suffer the same problems when constraints block inference entirely (unless we do something smart here).
# Drawbacks
It's a new symbolic operator. Its meaning may not be immediately intuitive to a first-time reader.
# Alternatives
* Proposal #138 (Qualified member-access expressions)
* Proposal #116 (Auto-awaited expressions)
* Proposal #59 (Post-fix casting operator)
# Unresolved Questions
* Should we use `|>` like ML languages instead of `->`? **No**
* Can you pipe directly into a default property? **Yes**
* Can you elide empty argument lists when piping? **Yes, for consistency**
* Can the left operand of -> be implicit via a `With` block? **Um, maybe...**
* Should `->` return the left operand if the target method is a `Sub` to enable fluent use of non-fluent APIs? **No, because it would only work with `Sub` targets and would require a different solution for `Function` targets; lame**
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.