dotnet / vblang

The home for design of the Visual Basic .NET programming language and runtime library.
286 stars 63 forks source link

Block expressions #109

Open AnthonyDGreen opened 7 years ago

AnthonyDGreen commented 7 years ago

VB is a statement oriented language but sometimes a computation is difficult to express purely expressions. We've attacked this problem before with the If expression but that's always left open the question of whether we should do it for Select Case too. T-SQL has a CASE expression and the topic has been raised again when considering whether an expression form of pattern matching would be needed.

I'm not a big fan of creating a new expression based syntax for everything and I do think preferring statements is more readable. That said, it's worth considering what a potential design would do to the language. Taking a cue from piecewise function notation this proposal is using { and } to delineate a statement block. Believe it or not but there are surprisingly few ambiguities with these characters. The minimal feature we could consider would be Select Case:

Dim timeOfWeek = { Select Case Date.Today.DayOfWeek
                       Case DayOfWeek.Monday To DayOfWeek.Friday
                           "Work Week"
                       Case DayOfWeek.Saturday, DayOfWeek.Sunday
                           "Weekend"
                   End Select }

It seems straight-forward enough what's happening, maybe? It also solves the problem some languages solve with a Let statement and would avoid adding a specific Select Case or Match expression.

Should we have a special keyword for yielding the ultimate value of the expression (e.g. Yield). I suspect Return wouldn't be ambiguous (since today it can't appear in the middle of an expression) but it could be confusing. What if instead we take a page from the F# manual and say that the value of the block is the value of the last expression statement along an execution path.

That's Select Case but if we do that what if we redid If? It would solve scenario #97 without changing the behavior of the If expression:

Dim b As Boolean? = { If crmDate.IsNull Then Nothing Else crmDate.Value }

Inspired by #57 what if we supported more than just Select Case and If, what if we supported loops?

Return <xml>
           <%= { For Each item in aList
                     <NodesiNeed fora="item" />
                 Next } %>      
       </xml>

That's pretty nice looking, I think. Of course, in this case a loop statement would yield and IEnumerable instead of a single value. Is that too subtle? (YES!) What if we required the Yield statement to create an IEnumerable? And maybe we could even just do more statements if we can work out ambiguities.

The reason this could work without syntactic ambiguity is because every executable statement in Visual Basic begins with a keyword with the exception of invocation and assignment which may begin with an identifier, and labels which may begin with an identifier or number so the parser can tell pretty quickly if the curlies contain a statement list or an expression list.

Needs a prototype and we need to see code where this would make the code more readable to determine whether this would make VB more expressive without sacrificing straight-forwardness.

AdamSpeight2008 commented 7 years ago

Using { ... } to denote a block could be an issue, as it { ... } is currently treated as an array / collection literal. In your first example the inferred type of timeOfWeek would be String() not String.

Select Case could be treat as an expression, if it is a position you'd expect an expression.

Dim timeOfWeek = Select Case Date.Today.DayOfWeek
                     Case DayOfWeek.Monday To DayOfWeek.Friday
                          "Work Week"
                     Case DayOfWeek.Saturday, DayOfWeek.Sunday
                          "Weekend"
                 End Select

or

Function TypeOfDay() As String
  Return Select Case Date.Today.DayOfWeek
                  Case DayOfWeek.Monday To DayOfWeek.Friday
                    "Work Week"
                  Case DayOfWeek.Saturday, DayOfWeek.Sunday
                    "Weekend"
         End Select
End Function
AnthonyDGreen commented 7 years ago

Dropping the {} is another reasonable approach. It means only supporting a single statement, which might be plenty. I'm not sure how I would analyze a large code base to see what benefit, if any, would be gained by such a feature. Maybe looking for cases where Case blocks are just initializing the same variable to different values (rather than doing different things).

AdamSpeight2008 commented 7 years ago

There is an ambiguity in the following code.

Function F()
  Dim timeOfWeek =
  Select Case Date.Today.DayOfWeek
    Case DayOfWeek.Monday To DayOfWeek.Friday
      Return "Work Week"
    Case DayOfWeek.Saturday, DayOfWeek.Sunday
      Return "Weekend"
  End Select
End Function

Is this an error; Incomplement statement Dim timeOfWeek = followed be a valid select case block. Or a statement Select Case block initialising the variable declaration, which is using implicit line continuation?

rskar-git commented 7 years ago

@AdamSpeight2008 I'm curious, for this new "Block expressions" form of Select Case, can the compiler be made to accept it but only if the Select Case immediately follows a parenthesis?:

Dim timeOfWeek = ( Select Case Date.Today.DayOfWeek
                       Case DayOfWeek.Monday To DayOfWeek.Friday
                           "Work Week"
                       Case DayOfWeek.Saturday, DayOfWeek.Sunday
                           "Weekend"
                    End Select)

Would that help out the ambiguities you showed?

Also, if that is possible, could this "immediately follows a parenthesis" concept open the door to inline assignments and perhaps other ideas?:

Dim timeOfWeek As String
Console.Write("Hello friend, enjoy your " + (Let timeOfWeek =
    ( Select Case Date.Today.DayOfWeek
                       Case DayOfWeek.Monday To DayOfWeek.Friday
                           "Work Week"
                       Case DayOfWeek.Saturday, DayOfWeek.Sunday
                           "Weekend"
        End Select))
AnthonyDGreen commented 7 years ago

If one situation is an error and the other is not it's not an ambiguity.

AnthonyDGreen commented 7 years ago

Similarly, it's not an issue to use {} because an array literal never could have contained a select case before. When you parse the { you peek one token ahead. If it's a Select it's a block and if not it's an array literal.

AdamSpeight2008 commented 7 years ago

The first would not have compiled, the second would. Hence the breaking change.

AnthonyDGreen commented 7 years ago

Which is the second?

AdamSpeight2008 commented 7 years ago

First is using current implement (ie no block expression). Second is with. So without changing a single character, the code has a different meaning.

AnthonyDGreen commented 7 years ago

Could you paste in the specific code you're talking about. I want to be sure we're talking about the same example.

AdamSpeight2008 commented 7 years ago
Function F()
  Dim timeOfWeek =
  Select Case Date.Today.DayOfWeek
    Case DayOfWeek.Monday To DayOfWeek.Friday
      Return "Work Week"
    Case DayOfWeek.Saturday, DayOfWeek.Sunday
      Return "Weekend"
  End Select
End Function
AnthonyDGreen commented 7 years ago

That code doesn't compile today, with the feature it would compile. This is not a breaking change. It's just a change--a non-breaking change; it's a new feature. Most new features are taking something that didn't compile and making them compile, so in 2005:

Dim xml = <html></html>

Would just be a bunch of parse errors and in 2008 it means something. A breaking change specifically means the program compiled and executed one way in one version and either stopped compiling entirely in the next or compiled and executed differently than before. Since your code example didn't compile and can't execute today it can't be a breaking change.

And while there are no ambiguities in the code example (it can only mean the valid parse) it's even clearer in my original proposal specifically because the { and } delimiters are required. And there is no ambiguity using those characters because an array literally can't include a Select Case statement in its element list, therefore any valid compiling VB program where { Select Case ... } appears cannot mean anything but a block expression. And it wouldn't mean a String array instead of a String because that's not how the feature is designed as proposed, it's only because your alternate proposal removes the delimiters { and } that the behavior now has to compose with array literals that its meaning would be to create a string array. It's very critical that we be precise with the terminology since overusing the term breaking change invokes a lot of machinery and scrutiny at Microsoft.

( and ) work for the same reason, there is no meaning to a parenthesized Select Case block today.

The reason the delimiters are valuable is if you want to allow multi-statement blocks:

Dim iterator = { Dim a = 0, b = 1
                 Yield a
                 Yield b
                 Do
                     Yield a + b
                     b = a + b : a = b - a
                 Loop }

In this case, it can only work if we require the delimiters or only the first statement would be part of the "block" and the others would not.

AdamSpeight2008 commented 7 years ago

Because the existing usage of { } to mean collection initialiser / array literal. The following code

Dim timeOfWeek = { Select Case Date.Today.DayOfWeek
                       Case DayOfWeek.Monday To DayOfWeek.Friday
                           "Work Week"
                       Case DayOfWeek.Saturday, DayOfWeek.Sunday
                           "Weekend"
                   End Select }

is likely to interpreted as roughly equivalent to

Dim timeOfWeek As String() = { "Work Week" }
' or 
Dim timeOfWeek As String() = { "Weekend" }

and not as

Dim timeOfWeek As String = "Work Week"
'or 
Dim timeOfWeek AS String = "Weekend"

Especially confusing when use in multi-dimensional array literals, how many dimensions is it?

Dim numbers =
{
  {  {
       Select Case w
         Case Is < 0: -1
         Case Is = 0:  0
         Case Else  :  1
       End Select
     } ,
     {
       Select Case x
         Case Is < 0: -1
         Case Is = 0:  0
         Case Else  :  1
       End Select
    }
  } ,
  {
    {
      Select Case z
        Case Is < 0: -1
        Case Is = 0:  0
        Case Else  :  1
      End Select
    },
    {
      Select Case z
        Case Is < 0: -1
        Case Is = 0:  0
        Case Else  :  1
      End Select
    }
  }
}

Parenthesis usage could cause confusion with Tuple Liteals.


I think Select Case block (no delimiters) could be allowed if it isn't preceded by a implicit line continuation. Then require an Explicit Line Continuation eg

Function F()
  Dim timeOfWeek = _ 
  Select Case Date.Today.DayOfWeek
    Case DayOfWeek.Monday To DayOfWeek.Friday
      Return "Work Week"
    Case DayOfWeek.Saturday, DayOfWeek.Sunday
      Return "Weekend"
  End Select
End Function
AdamSpeight2008 commented 7 years ago

Wouldn't VB style suggest delimiters using words? ie

AnthonyDGreen commented 7 years ago

Crap, I might have accidently proposed adding what we were calling sequence expressions in C# (but ultimately cut) two releases ago to VB.

* Goes off to rethink his life *

ghost commented 5 years ago

@AnthonyDGreen @AdamSpeight2008 @rskar-git @srivatsn @migueldeicaza It is doable today like this:

Dim timeOfWeek = (Fumction(x)
                Select Case X
                       Case DayOfWeek.Monday To DayOfWeek.Friday
                           return "Work Week"
                       Case DayOfWeek.Saturday, DayOfWeek.Sunday
                           return "Weekend"
                 End Select 
End Function) (Date.Today.DayOfWeek)

But it will be nice to simplify this syntax.. I suggest:

Dim timeOfWeek = Evaluate(x = Date.Today.DayOfWeek) As string
                Select Case X
                       Case DayOfWeek.Monday To DayOfWeek.Friday
                           return "Work Week"
                       Case DayOfWeek.Saturday, DayOfWeek.Sunday
                           return "Weekend"
                 End Select 
End Evaluate

The Evaluate block is a lamda expression with suplied values for its parameters. this aloow us to evaluate any expression not just Select case, and work on any number of variables in the expresion.

ghost commented 5 years ago

If the Evaluate end Evaluate is not accepted, it can be done by the Function end function keywords, say like this:

Dim timeOfWeek = Function(x As DayOfWeek := Date.Today.DayOfWeek) As string
                Select Case X
                       Case DayOfWeek.Monday To DayOfWeek.Friday
                           return "Work Week"
                       Case DayOfWeek.Saturday, DayOfWeek.Sunday
                           return "Weekend"
                 End Select 
End Function
VBAndCs commented 5 years ago

@AnthonyDGreen I Like the concept, but I prefere to use a more VB Style by using lambda properties:

Dim timeOfWeek = Get
              Select Case Date.Today.DayOfWeek
                       Case DayOfWeek.Monday To DayOfWeek.Friday
                           "Work Week"
                       Case DayOfWeek.Saturday, DayOfWeek.Sunday
                           "Weekend"
                   End Select 
           End Get

That is my #398 proposal.

And inside XML literals, I see no need to use any block more than the <%= %> it self!

Return <xml>
           <%= For Each item in aList
                     <NodesiNeed fora="<%= item %>" />
                 Next  %>      
       </xml>

This is my #397 proposal.

And if we have to use the returned Value in an expression (Which is not mosf likely inside XML literals), then we can use the Get End ... GET.

VBAndCs commented 5 years ago

Another important addition is a standalone scope (code block). I suggested to define it using Let ... End LET in #393 and found a good use of it insie XML leterals as in here: #397 (comment)

pricerc commented 5 years ago

Firstly: Just please no more squiggly brackets (or any other new punctuation). It's bad enough they got used for the initializer's With, when an End With would have been much more 'VB'.

On its own, SELECT returning a value, like SQL's CASE, has merit, but I can see it being abused, and (IMHO) anything more complicated than the examples in this thread would probably be better served by using a discrete method, for which local functions (which have cropped up elsewhere in this forum recently) might be more legible than an monster block expression.

To ensure my dead horse remains flogged: I don't think VB as a language is improved by programming shortcuts introduced for the sake of saving a few keystrokes during development (which this looks a bit like), if the end result is more difficult for the maintainer of the software five years later after you've left the company.

And as @AnthonyDGreen said in his opening comment:

VB is a statement oriented language

That's a language paradigm choice, changing that changes the language into a new language.

bandleader commented 5 years ago

I'm fine with using a keyword instead of curly braces, but how would this work for continuing the expression?

'This works pretty well...
Dim nextId = {
  'Get last ID
  Dim lastRecord = database.GetLastRecord()
  Dim lastId = lastRecord.Id
  Return lastId
} + 1

'This works less...
Dim nextId = Get
  'Get last ID
  Dim lastRecord = database.GetLastRecord()
  Dim lastId = lastRecord.Id
  Return lastId
End Get + 1 'what??

Or do we say that using doing any other processing along with an expression block is using an anti-pattern?

pricerc commented 5 years ago

Or do we say that using doing any other processing along with an expression block is using an anti-pattern?

Yes.

jrmoreno1 commented 5 years ago

@pricerc: I don't want a select expression to save characters, I want a select expression to increase readability. If I have =x = select i case .... then I know that the purpose of the select block is to get a value to be assigned to x. If it's not an expression, and is just a block, then it could be doing anything in there, and I have to read through all of them to make sure that they are doing the same thing. With a select expression, the expression just becomes a dictionary with possibly some extra logic in it.

pricerc commented 5 years ago

I want a select expression to increase readability.

I understand that, which is why I said:

On its own, SELECT returning a value, like SQL's CASE, has merit, but I can see it being abused, and (IMHO) anything more complicated than the examples in this thread would probably be better served by using a discrete method, for which local functions (which have cropped up elsewhere in this forum recently) might be more legible than an monster block expression.

I don't know why my last response was quite so terse. Let me expand on it.

I'm fine with using a keyword instead of curly braces, but how would this work for continuing the expression?

'This works pretty well...
Dim nextId = {
  'Get last ID
  Dim lastRecord = database.GetLastRecord()
  Dim lastId = lastRecord.Id
  Return lastId
} + 1

'This works less...
Dim nextId = Get
  'Get last ID
  Dim lastRecord = database.GetLastRecord()
  Dim lastId = lastRecord.Id
  Return lastId
End Get + 1 'what??

Or do we say that using doing any other processing along with an expression block is using an anti-pattern?

Yes. I'd argue that this pattern works better:

Function GetNextID() As ID
    Dim lastRecord = database.GetLastRecord()
    Dim lastId = lastRecord.Id
    Return lastId
End Function

....
Dim nextId = GetNextID()

Where GetNextID could be a local function, which I like the idea of and I think would be a better solution than the anonymous functions that block expressions are necessarily converted into by the compiler.

bandleader commented 5 years ago

@pricerc Fair enough, and I agree in the specific example mentioned above. I was just throwing out the question of whether there's EVER a legitimate use of block expressions within another expression, or whether that's ALWAYS an anti-pattern.

would be a better solution than the anonymous functions that block expressions are necessarily converted into by the compiler.

@pricerc Again, I agree with you in this specific case, but I just want to point out that 1) anonymous functions often do indeed improve readability, for those who use them for what they're there for, and inthose cases factoring them out to a separate method decreases readability rather than increase it 2) as far as I understand, compilers do not need to convert block expressions to anonymous functions

pricerc commented 5 years ago

@pricerc Fair enough, and I agree in the specific example mentioned above. I was just throwing out the question of whether there's EVER a legitimate use of block expressions within another expression, or whether that's ALWAYS an anti-pattern.

I don't know that I'd commit to ALWAYS, because in my life's experience, there are always exceptions to prove a rule.

  1. anonymous functions often do indeed improve readability, for those who use them for what they're there for, and inthose cases factoring them out to a separate method decreases readability rather than increase it

To be fair, I don't go hunting for them, but I've yet to see an example of an anonymous function where its readability would be reduced by refactoring it out into a named method. I'd say at best anonymous function have nil effect on readability, but never inherently positive. But I do concede this to be subjective, and don't judge people for using them :).

  1. as far as I understand, compilers do not need to convert block expressions to anonymous functions

I would think that would depend on the complexity of the block. It certainly introduces a new scope, which needs to be managed and potentially debugged.

VBAndCs commented 5 years ago

I have another approach for this: I suggest to use the statement Me.Return to return a value from the smallest block expression enclosing it. And to have more control ovet nested expression blocks, we can use the name of the outer expression block like Do.Return, If.Return and for.Return…. etc. This will allow having block expressions without ant further decoration. Examples: Dim x = (Me.Return 3) Which is equivalent to: Dim x = 3

Dim y = Select case x
                     case 1
                           Me.Return "Once" 
                      case 2
                           Me.Return "Twice"
                      case else
                           Me.Return "Many Times"
                   End Select
Do
     ' some code
     If Not (Do
                     ' some code
                     If someCondition Then Me.Return false
                 Loop) Then Exit Do     
Loop

Notes:

  1. If statement here is not used as an expression, so, me refers to the Do because it is the nearest block expression.

  2. We can use Me.Yields inside loops to yield an enumerable.

pricerc commented 5 years ago

@VBAndCs Me is for the current instance of a class. Any other use I would call a 'bad idea'.

And if you were going to do that much Returning, you may as well just use a function.

VBAndCs commented 5 years ago

This proposal combined with @AnthonyDGreen 's Top-Level Code Prototype can achieve this proposal like this:

Dim timeOfWeek = <( Select Case Date.Today.DayOfWeek
                       Case DayOfWeek.Monday To DayOfWeek.Friday
                           "Work Week"
                       Case DayOfWeek.Saturday, DayOfWeek.Sunday
                           "Weekend"
                   End Select )>

and many more, like embedding code in interpolated strings

Dim Msg= $" time Of Week is {<( Select Case Date.Today.DayOfWeek
                       Case DayOfWeek.Monday To DayOfWeek.Friday
                           "Work Week"
                       Case DayOfWeek.Saturday, DayOfWeek.Sunday
                           "Weekend"
                   End Select )>}"