dotnet / dotnet/vblang

Pipe-Forward Operator

Open
#165 13 comments 8 reactions 1 assignee Claimed by @AnthonyDGreen View on GitHub
Proposal Prototype-Needed Ready-to-Review
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.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.