Skip to content
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

In Expressions #229

Closed
AdamSpeight2008 opened this issue Dec 24, 2017 · 19 comments
Closed

In Expressions #229

AdamSpeight2008 opened this issue Dec 24, 2017 · 19 comments
Labels
LDM Reviewed: No plans LDM has reviewed and this feature is unlikely to move forward in the foreseeable future

Comments

@AdamSpeight2008
Copy link
Contributor

In Expression

An In expression permits a single value (potentially from an expression) to validate if it contained in t a collection like expression.

Syntax InExpr ::= (InKeyword : InKeywordSyntax)(Expr : ExpressionSyntax);
Implicit Parenthesises :- ( In (expr) )

Examples

TypeOf Multiple (#93)

If TypeOf obj Is In { T0, T1, T2 } Then ...
If TypeOf obj IsNot In { T0, T1, T2 } Then ...

With Range Expression (#25)

Dim range = 0 To 10 
If value Is In range Then ...

Enum Flags (#228)

If (Color.Red Is In value) OrElse (Color.Blue Is In value) Then ...
If (Color.Red IsNot In value) OrElse (Color.Blue IsNot In value) Then ...
@pricerc
Copy link

pricerc commented Dec 27, 2017

Thanks for addressing one of my pet niggles.

My first formal programming training was in Pascal 30-odd years ago, so sets come naturally to me. And these days I spend more time in SQL than any other language.

So if there's one thing I've often missed in VB, it's an 'In' construct for dealing with sets (collections in VB-speak). To the point where I once wrote an extension method to wrap 'Contains', so that I could do:

If value.InSet({Color.Red, Color.Blue}) Then...

But extending that to Enum Flags(bitmasks) would be an welcome addition. Now if you could contract it a bit more, it would be extra awesome. Something like:

If ({Color.Red, Color.Blue}) Is In value Then...
or
If (Color.Red Or Color.Blue) Is In value Then...

I think reads a little more naturally than:
If (((Color.Red Or Color.Blue) And Value) <> 0) Then...

@zspitz
Copy link

zspitz commented Feb 14, 2018

I like the idea of an In operator. However, I would suggest that In would be more flexible as a syntactic sugar for a Contains instance or extension method as well.

This would cover, out of the box, the most obvious use case for In -- If x Is In IEnumerable<T>.

It would also allow individual types to implement their own Contains logic; which would cover the range expression use case. The Range object you describe here which would be output from a range expression, could have its own Contains instance method that would check the parameter against the upper and lower bounds of the range, and if it matches the step.


Question: Is x Is In y preferable to x In y?

@zspitz
Copy link

zspitz commented Feb 14, 2018

@pricerc What does this:

If (Color.Red Or Color.Blue) Is In value Then...

mean, in current VB.NET syntax? This:

If (value And Color.Red) = Color.Red OrElse (value And Color.Blue) = Color.Blue Then ...

or this:

Dim colors = Color.Red Or Color.Blue
If (colors And value) = colors Then ...

@pricerc
Copy link

pricerc commented Feb 14, 2018

... had to re-read the whole thread to remember what this discussion was about - using Color.Red/Green/Blue is a bit distracting because I've also done a bit of work ORing and ANDing RGB values within Color structures...

@zspitz .

Re: your first comment: the basic "Item In {Set}" should indeed just be syntactic sugar over Contains.

Then regarding your question re:
If (Color.Red Or Color.Blue) Is In value Then...

At the time, I was thinking about the construct I had in my comment:

If (((Color.Red Or Color.Blue) And Value) <> 0) Then...

So, that equates to your first example:

If (value And Color.Red) = Color.Red OrElse (value And Color.Blue) = Color.Blue Then ...

But I think I need to flesh it out some more into a more complete example that doesn't use Red Green and Blue for bit flag names.

@pricerc
Copy link

pricerc commented Feb 14, 2018

Please excuse the length of this post, but I needed an example that was closer to something I've used.

Given an enum (potentially fairly long) declared thus:

<Flags>
Public Enum TransactionTypes
    None

    ArSalesOrder = 1 << 0 ' &B0000_00000000_00000001
    ArDelivery = 1 << 1 ' &B0000_00000000_00000010
    ArSalesInvoice = 1 << 2 ' &B0000_00000000_00000100
    ArPayment = 1 << 3 ' &B0000_00000000_00001000
...
    ApPurchaseOrder = 1 << 8 ' &B0000_00000001_00000000
    ApPurchaseReceipt = 1 << 9 ' &B0000_00000010_00000000
    ApPurchaseInvoice = 1 << 10 ' &B0000_00000100_00000000
    ApPayment = 1 << 11 ' &B0000_00001000_00000000
...
End Enum

You can have a transaction class and processor that looks like this:

    Public Class ArTransaction
        Implements ITransaction

        Public Shared ReadOnly ValidTransactionTypes As TransactionTypes() = {
            TransactionTypes.ArSalesOrder,
            TransactionTypes.ArDelivery,
            TransactionTypes.ArSalesInvoice,
            TransactionTypes.ArPayment
        }

        Dim _transactionType As TransactionTypes
        Property TransactionType As TransactionTypes Implements ITransaction.TransactionType
            Get
                Return _transactionType
            End Get
            Set
                ' force use of one and only one type, array doesn't contain multi-value flags.
                If Not ValidTransactionTypes.Contains(Value) Then
                    Throw New ArgumentOutOfRangeException()
                End If

                _transactionType = Value
            End Set
        End Property

        Public Property TransactionData As String Implements ITransaction.TransactionData
    End Class

    Public Class TransactionProcessor
        ' process a queue of transactions, filtered by the specified range of types, supplied as a flag enum value
        Public Function ProcessTransactions(transactionTypesToProcess As TransactionTypes, TransactionList As List(Of ITransaction)) As TransactionTypes
            'TODO: 
            ' validate transactionTypesToProcess vs ArTransaction.ValidTransactionTypes

            Dim result As TransactionTypes = TransactionTypes.None

            For Each currentTransaction As ITransaction In TransactionList
                If ValidateTransactionType(transactionTypesToProcess, currentTransaction.TransactionType) Then
                    ProcessTransaction(currentTransaction)
                    result = result Or currentTransaction.TransactionType
                End If
            Next
            Return result
        End Function

        Private Function ValidateTransactionType(transactionTypesToProcess As TransactionTypes, transactionType As TransactionTypes) As Boolean
            Return (transactionTypesToProcess And transactionType) = transactionType
        End Function

        Private Sub ProcessTransaction(currentTransaction As ITransaction)
            Throw New NotImplementedException()
        End Sub
    End Class

If, however, we could have a set construct (the 'IN' operator being proposed) that works on flag enums, then a few parts (IMO) look a bit 'cleaner':

    Public Class ArTransaction
        Implements ITransaction

        Public Shared ReadOnly ValidTransactionTypes As TransactionTypes =
            TransactionTypes.ArSalesOrder Or
            TransactionTypes.ArDelivery Or
            TransactionTypes.ArSalesInvoice Or
            TransactionTypes.ArPayment

        Dim _transactionType As TransactionTypes
        Property TransactionType As TransactionTypes Implements ITransaction.TransactionType
            Get
                Return _transactionType
            End Get
            Set
                ' This will allow a multi-value flag, which is possibly not desirable.
                ' To force use of one and only one flag, would require counting the set bits
                ' in the underlying bit array and making having more than one an exception
                If Not Value In ValidTransactionTypes Then
                    Throw New ArgumentOutOfRangeException()
                End If

                _transactionType = Value
            End Set
        End Property

        Public Property TransactionData As String Implements ITransaction.TransactionData
    End Class

    Public Class TransactionProcessor
        ' process a queue of transactions, filtered by the specified range of types, supplied as a flag enum value
        Public Function ProcessTransactions(transactionTypesToProcess As TransactionTypes, TransactionList As List(Of ITransaction)) As TransactionTypes
            'DONE: 
            ' validate transactionTypesToProcess vs ArTransaction.ValidTransactionTypes
            If Not transactionTypesToProcess In ArTransaction.ValidTransactionTypes Then
                Throw New Exception()
            End If

            Dim result As TransactionTypes = TransactionTypes.None

            For Each currentTransaction As ITransaction In TransactionList
                If currentTransaction In transactionTypesToProcess Then
                        ProcessTransaction(currentTransaction)
                    result = result Or currentTransaction.TransactionType
                End If
            Next
            Return result
        End Function

        Private Sub ProcessTransaction(currentTransaction As ITransaction)
            Throw New NotImplementedException()
        End Sub
    End Class

of course the semantics of 'ValidTransactionTypes' is slightly different, which is my one concern about the concept, that I need to think through some more after a good night's sleep.

@AdamSpeight2008
Copy link
Contributor Author

@pricerc I've added your example using Enum Flag Operators in #228.

@AdamSpeight2008
Copy link
Contributor Author

@pricerc I'm not against extending In to any collection the supports .Contains( value As T) As Boolean.
I do have concerns that it could make the checking inefficient.

@zspitz
Copy link

zspitz commented Feb 14, 2018

@AdamSpeight2008

I do have concerns that it could make the checking inefficient.

In could map to the most appropriate matching method -- instance or extension -- and follow the same rules the compiler uses to select between overloads (e.g. instance method before extension method). If a specific type implements a Contains method with the appropriate signature, In would map to the instance method, over a compatible extension method.

WRT range expressions, as I noted here, the range expression could compile to a Range object which would have an appropriate Contains method, which would work efficiently.


This architecture would also allow third-parties to implement type-specific extension methods, e.g. for `Dictionary(Of TKey, TValue):

<Extension()> Function Contains(Of TKey, TValue)(dict As Dictionary(Of TKey, TValue), key As TKey) As Boolean
    Return dict.ContainsKey(key)
End Function

which could then be called with In:

Dim dict As New Dictionary(Of String, Integer)
If "a" In dict Then ...

which would map to:

If dict.Contains(a) Then ...

The mapping of LINQ query syntax to method syntax works the same way, using instance methods (and preferring them over extension methods):

Sub Main
    Dim qry = 
        From i In New LINQable
        Where i > 5
        Select Cstr(i)
End Sub

Class LINQable
    Function [Select](fn As Func(Of Integer, String)) As LINQable
        Console.WriteLine("You called the Select function")
        Return Me
    End Function
    Function [Where](fn As Predicate(Of Integer)) As LINQable
        Console.WriteLine("You called the Where function")
        Return Me
    End Function
End Class

prints:

You called the Where function
You called the Select function

@AdamSpeight2008
Copy link
Contributor Author

@zspitz
I know about LINQ expressions / extensions but they do come with downsides like array and enumerator allocations. My concern is on how the following is lowered / transformed

dim ok = x In { 1, 2, 3 }
  • Use .Contains
    • Allocates an array.
dim ok = {1, 2, 3 }.Contains( x )
  • Use the Enumerable.Any extension method on Enumerable
    • Produces a delegate object.
dim ok = {1.2.3}.Any( Function( _x ) _x = x 
  • Use explicit comparison with equals operator.
    • requires verification the T as the binary operator = that return boolean
    • or alternatively use OrElse?
dim ok = ( x = 1) Or (x=2) Or (x=3)
  • Use explicit comparison with .ComparedTo( )
    • require verification that the T implements IComparable(Of T).
      • May involve boxing.
    • or alternatively use OrElse?
dim ok = (x.ComparedTo( 1 )= 0 Or x.CompartedTo( 2 )=0  Or x.ComparedTo( 3 )=0

How to choose which to use and in what situations?


In the Dictionary(Of TKey, TValue) case it easier to write efficient code.

Dim dict As New Dictionary(Of String, Integer)
If "a" In dict Then ...
   Dim value = dict("a")

instead of

Dim dict As New Dictionary(Of String, Integer)
Dim value As Integer = Nothing
If dict.TryGetValue("a", value) In dict Then ...

which could be written "better" with Out Arguments

Dim dict As New Dictionary(Of String, Integer)
If dict.TryGetValue("a", Out value) In dict Then ...

@zspitz
Copy link

zspitz commented Feb 28, 2018

@AdamSpeight2008

My concern is on how the following is lowered / transformed:
dim ok = x In { 1, 2, 3 }

You are proposing four possible rewrites:

  1. Dim ok = { 1, 2, 3 }.Contains(x)
  2. Dim ok = {1.2.3}.Any( Function( _x ) _x = x)
  3. Dim ok = ( x = 1) OrElse (x=2) OrElse (x=3)
  4. Dim ok = x.ComparedTo(1)= 0 OrElse x.CompartedTo(2)=0 OrElse x.ComparedTo( 3 )=0

I don't see any possible benefit in rewriting to 2; and, as you say, it introduces a new delegate object for no real benefit.

Also, if we are already going to the trouble of rewriting to 3 or 4, I don't see any reason to use Or instead of OrElse.


If I understand correctly, the benefit of 3 or 4 over 1 is that 1 has a potentially expensive array allocation, which is saved by the rewrite of 3 and 4. However, I would suggest that since the following is an array allocation in VB.NET (as well as assignment to the implicitly typed variable arr:

Dim arr = {1,2,3,4}

then an implicit array allocation in this syntax:

Dim ok = x In {1,2,3,4}

isn't introducing any surprising behavior, and could thus use the simpler rewrite to Contains, This allows a simpler mental model of how In would work-- this:

an In expression maps to an appropriate Contains method

is simpler to express and understand, than

an In expression maps to logical OrElse clauses when written against a fixed set of literals, otherwise maps to the appropriate Contains method

@zspitz
Copy link

zspitz commented Feb 28, 2018

@AdamSpeight2008

If I understand you correctly, you are concerned that in the specific case of checking for a key in a Dictionary, mapping In to Contains might result in a double check when the value is later extracted: once by a call to Contains (which would call ContainsKey) and then again with a call to GetValue; e.g.

Dim dict As New Dictionary(Of String, Integer)
If "a" In dict Then
   Dim value = dict("a")

would map to:

Dim dict As New Dictionary(Of String, Integer)
'Contains is a theoretical extension method, taking only the key
If dict.Contains("a") Then     'key existence check 1
   Dim value = dict("a")        'key existence check 2

I would suggest that:

  • if the value is of interest, then it would be better to use Out parameters, or TryGetValue
  • if the value is unimportant, and all we want to know is whether the key exists or not, then ContainsKey (and by extension the theoretical In) would be the appropriate idiom.

@WolvenRA
Copy link

@ zspitz "Question: Is x Is In y preferable to x In y?" Yes, Absolutely!

If x Is In y Then...

If Color.Red Is In Colors Then...

If (Color.Red Or Color.Blue) Is In Colors Then...

@KathleenDollard
Copy link
Contributor

@WolvenRA

Your third example is problematic because VB behavior will diverge from English semantics:

If (Color.Red Or Color.Blue) Is In Colors Then...

This would actually resolve to Purple and require that Purple be in Colors. I don't think that is what the line of code reads like in English.

@ocdtrekkie
Copy link

@KathleenDollard D: It is always a bit of a shock to me when I see an Or or And used that way in VB too. I can totally see people mixing that up.

@WolvenRA
Copy link

@KathleenDollard Thanx for pointing that out, but I must admit I'm not understanding why that would resolve to Purple (or any other color). Is it because of the parenthesis or because it should read;

If Color.Red Is In Colors OR Color.Blue Is In Colors Then...   

(Which actually does make more sense)

@ocdtrekkie
Copy link

@WolvenRA That would work better, I think yes.

So the issue is that Or is also a bitwise operator, which I always personally forget and never use as such. Since a Color type is a bit format: "The color of each pixel is represented as a 32-bit number: 8 bits each for alpha, red, green, and blue (ARGB). Each of the four components is a number from 0 through 255, with 0 representing no intensity and 255 representing full intensity."

An Or on two colors will result in a combined Color, where if any bit exists in either color, it is in the resulting color. So the red bits and the blue bits are both in the combined output of the Or of the two, and hence, you get purple.

A good way to avoid accidentally doing this and not seeing what you did wrong, I think, is to use OrElse, which only works for boolean values (such as in a conditional like this), and has the added benefit of being a short circuit operator which won't waste time evaluating the second condition if the first matches.

@WolvenRA
Copy link

@ocdtrekkie Thanx for the explanation. I do use OrElse... for the short circuiting reason.

@pricerc
Copy link

pricerc commented Mar 20, 2018

this confusion around Color is exactly why I tried to describe a different scenario, since I don't think Color is really the target 'market' for this discussion, not helped by the fact that colors are things that we use that aren't actually a flags enum, which is actually what the discussion was about...

so while

If (Color.Red Or Color.Blue) Is In Colors Then...

may not make a lot of sense. This might make more (depending on what you're trying to achieve):

<Flags> Enum WidgetAttributes
None = 0
Solid = 1
Perforated = 2
Flexible = 4
Transparent = 8
Patterned = 16
...
End Enum

...
' WidgetAttributes enum name removed for clarity
'
AllowedWidgetAttributes = Solid or Transparent or Patterned or Flexible
...
Widget.Attributes = Solid or Transparent

If Widget.Attributes Is In AllowedWidgetAttributes Then...

where Widget.Attributes and AllowedWidgetAttributes are both of type WidgetAttributes, and Widget.Attributes is required to be subset of AllowedWidgetAttributes.

@KathleenDollard KathleenDollard added the LDM Reviewed: No plans LDM has reviewed and this feature is unlikely to move forward in the foreseeable future label Jun 13, 2018
@KathleenDollard
Copy link
Contributor

I'd like to discuss these in the discussion on the original issues (indicated) and discuss syntax in that context.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LDM Reviewed: No plans LDM has reviewed and this feature is unlikely to move forward in the foreseeable future
Projects
None yet
Development

No branches or pull requests

6 participants