Search code examples
vb.netgenericsicomparable

Is there a way to use ">" operator on a "T As IComparable(Of T)", instead of "a.GreaterThan(b)", or a way to simplify my wrapper line?


Consider:

MyClass(Of T As {New, IComparable(Of T)})

    Sub MySub(a As T, b As T)
        If a.CompareTo(b) > 0 Then
            ....
        End If
    End Sub
End Class

This can be made more readable by defining:

Public Module MyModule
        <System.Runtime.CompilerServices.Extension()> _
        Public Function GreaterThan(Of T As IComparable(Of T))(a As T, b As T) As Boolean
                Return (a.CompareTo(b) > 0)
        End Function
End Module

So the test becomes:

    If a.GreaterThan(b) Then

This is acceptable, but as part of more complicated expressions, it would be great to be able to define an operator, so could instead say

    If a > b Then

However the following definition in MyClass:

    Public Shared Operator >(a As T, b As T) As Boolean
        Return (a.CompareTo(b) > 0)
    End Operator

yields compile-time error "At least one parameter of this binary operator must be of the containing type..."

Is there any alternative way to make this possible?


THE ALTERNATIVE I HAVE SO FAR (but isn't 100% satisfactory):

Similar to this approach:

http://www.codeproject.com/Articles/8531/Using-generics-for-calculations

which creates a wrapper structure for the T - a structure containing a single field - and then defines operators on that wrapper. This makes it possible to do code that looks like this:

Public Sub MySub(valueT As T)
    Dim value As New Num(Of T, TNum)(valueT)
    If value > Me.MaxElement Then
        ...
    End If
End Sub

But having to wrap one of the values inside of Num() -- Dim value As New Num(Of T, TNum)(valueT) -- in order to get the > to compile is no more convenient than doing what I already have working:

Public Sub MySub(valueT As T)
     If valueT.GreaterThan(Me.MaxElement) Then
        ...
    End If
End Sub

So an alternative solution would be some way to make this line more elegant:

Dim value As New Num(Of T, TNum)(valueT)

The types involved in making Num work are inspired by the above reference, and by this approach:

https://stackoverflow.com/a/4834066/199364

which refers to Policy.I.cs and Policy.INumeric.cs within this:

https://citylizard.codeplex.com/

Here is a stripped down sketch of key types:

Public Interface INumeric(Of T As {New, IComparable(Of T)})
        Function Zero() As T
        ...
        Function Add(a As T, b As T) As T
        ...
        Function GreaterThan(a As T, b As T) As Boolean
        ...
End Interface
Public Structure Numeric
        Implements INumeric(Of Integer), 
                INumeric(Of Single), ...
        ...
        Public Function GreaterThan(ByVal a As Integer, ByVal b As Integer) As Boolean Implements INumeric(Of Integer).GreaterThan
                Return (a > b)
        End Function
        ...
        Public Function GreaterThan(ByVal a As Single, ByVal b As Single) As Boolean Implements INumeric(Of Single).GreaterThan
                Return (a > b)
        End Function
        ...
End Structure
' Wrapper, to simplify use of Structure Numeric.
Public Structure Num(Of T As {New, IComparable(Of T)}, TNum As {New, INumeric(Of T)})

        Public Shared ReadOnly tn As TNum = New TNum()

        Private ReadOnly value As T

        Public Sub New(a As T)
                Me.value = a
        End Sub


        ' implicitly convert "T" to "Num(Of T, TNum)"; e.g. "11" to "Num(Of Integer, ..) with value 11".
        Public Overloads Shared Widening Operator CType(a As T) As Num(Of T, TNum)
                Return New Num(Of T, TNum)(a)
        End Operator

        ' Implicitly convert "Num(Of T, TNum)" back to "T"; e.g. retrieve value "11".
        Public Overloads Shared Widening Operator CType(a As Num(Of T, TNum)) As T
                Return a.value
        End Operator

        ...
        Public Shared Operator <(a As Num(Of T, TNum), b As Num(Of T, TNum)) As Boolean
                Return tn.LessThan(a.value, b.value)
        End Operator
        ...
End Structure

and finally one can define MyClass:

Class MyClass(Of T As {New, IComparable(Of T)}, TNum As {New, INumeric(Of T)})
    Public Shared ReadOnly tn As TNum = New TNum()

    Public MaxElement As T = ...

    Public Sub MySub(valueT As T)
        Dim value As New Num(Of T, TNum)(valueT)
        If value > Me.MaxElement Then
                ...
        End If
    End Sub 

End Class

usage of MyClass:

Public Shared Sub Test()
    Dim v As New MyClass(Of Integer, Numeric)()
    ...
    v.MySub(99)
End Sub

The line I would like to eliminate or simplify is:

Dim value As New Num(Of T, TNum)(valueT)

This line is only there so that the > can work. By making one of the parameters to > be type Num(), the other parameter of type T gets automatically widened to also be Num(), and then the > is found.

Is there any way to change these definitions so that the above line would be simpler, or not needed?

Note that I don't want to require the PARAMETER to MySub to be Num() -- that would push the burden to code that shouldn't be concerned with this implementation detail -- it should work with T. Likewise, the other value used in > -- here, MaxElement -- should be of type T not Num(). At least in some cases; this is a simplified example.


Another Reference: An alternative starting point for generic numerics would have been Mark Gravell's Generic Operators using Linq Expressions, which is part of MiscUtil:

https://jonskeet.uk/csharp/miscutil/usage/genericoperators.html

But the internal code seen in Operator.cs is unfamiliar to me (ExpressionUtil.CreateExpression, Expression.Add), and I only need to support a handful of numeric types, so it was not worth understanding that approach, and evaluating its performance. Instead I hand-coded the few low-level methods I needed, for the few types I needed.

All the source materials for all of the approaches above are in C#; since I was incorporating into a VB dll, I was determined to end up with a VB solution, rather than reference an external DLL, to maximize the probability that JIT compiler will inline the simple methods that are involved. Perhaps someone more knowledgeable about the internals would have concluded they could use one of the existing C# dlls in a way that did not interfere with JIT.


NOTE: If there are any performance issues with the approach above, any performance-improving suggestions would also be great. This code is used with arrays having millions of elements (image analysis).

(Embedded in a large body of VB code that is mostly not time-critical, and it was more important to have programmer productivity / ease of changing some complicated custom algorithms; it so far hasn't been worth isolating a portion of the code for re-writing in C++, or using a numeric library. OK, that latter point is debatable, but regardless there would be a lot of custom VB formula-intensive code, that is deeply entwined with other legacy VB.Net code.)

Worst case, might have to use T4 to generate different versions of MyClass, one per numeric value type, instead of using Generics as shown. But I'd rather avoid that if possible -- there is a lot of code, over a number of classes, in the actual application. Was all UShort for small memory in 32-bit, but now also need Integer, Single, and Double versions.


Solution

  • One approach to simplifying the wrapping:

    Add a method to MyClass (or to an inherited base class with same T/TNum signature) to make the conversion more compact:

    Public Shared Function AsNum(valueT As T) As Num(Of T, TNum)
        Return New Num(Of T, TNum)(valueT)
    End Function
    

    Then can wrap parameter valueT more compactly:

    If AsNum(valueT) > Me.MaxElement Then
    

    Or if will be using repeatedly, and have type inference on:

    Dim value = AsNum(valueT)
    If value > Me.MaxElement Then
    

    Limitation:

    I have some classes that inherit from existing BaseClass(Of T), where I was hoping to avoid altering BaseClass to use the Num() wrapper. So they cannot get AsNum via inheritance. Each of these classes will need their own copy of AsNum method.

    AsNum can't be placed in a separate Module that works for all classes, i.e. Public Function AsNum (Of T As {New, IComparable(Of T)}, TNum As {New, INumeric(Of T)}) (valueT As T) As Num(Of T, TNum), because there is no way to infer type of TNum, except inside a class Of .. TNum.


    For arithmetic operators (+, *) in Structure Num, there is some question as to whether to return As T, or As Num(Of T, TNum). Currently I am returning T, because that seemed more efficient, since I am usually storing the result into a T. However, this doesn't work well in complicated expressions, because I keep having to wrap the intermediate values back into a Num():

    Structure Num
        ...
        Public Shared Operator +(a As Num(Of T, TNum), b As Num(Of T, TNum)) As T
            Return tn.Add(a.value, b.value)
        End Operator
    End Structure
        ...
    Class MyClass(Of T As {New, IComparable(Of T)}, TNum As {New, INumeric(Of T)})
        ...
        Public Shared Function MyFunc(a As T, b As T) As T
            Return AsNum(AsNum(a) * a) + AsNum(b) * b
        End Function
    End Class
    

    vs.

    Structure Num(Of T As {New, IComparable(Of T)}, TNum As {New, INumeric(Of T)})
        ...
        ' NOTE: The widening operator automatically converts the "T" from "tn.Add" to a "Num()".
        Public Shared Operator +(a As Num(Of T, TNum), b As Num(Of T, TNum)) As Num(Of T, TNum)
            Return tn.Add(a.value, b.value)
        End Operator
    End Structure
        ...
    Class MyClass(Of T As {New, IComparable(Of T)}, TNum As {New, INumeric(Of T)})
        ...
        Public Shared Function MyFunc(a As T, b As T) As T
            ' Each sub-expression needs at least one "Num()",
            ' but at least we no longer need a third "AsNum",
            ' for the "+" to work.
            Return AsNum(a) * a + AsNum(b) * b
        End Function
    End Class
    

    Using the second form above, which returns Num(): for some complicated methods, the cleanest solution might be to have two variants, with the T parametered variant calling the Num() parametered variant:

    Public Shared Function MyFunc(a As T, b As T) As T
        Return MyFunc(AsNum(a), AsNum(b))
    End Function
    Private Shared Function MyFunc(a As Num(Of T, TNum), b As Num(Of T, TNum)) As Num(Of T, TNum)
        Return a * a + b * b
    End Function
    

    The T variants are for external clients, which don't know about Num or TNum. The Num variant can be used internally, to build up calculations involving multiple methods.


    The point here is that we have a situation where it is worth a fair amount of one-time effort, if the end result is that the interior of lengthy methods has easy-to-read code even for complicated formulas, while maintaining good performance, and working with several different numeric types.

    It is also easier to incorporate existing code using operators, simply by altering declarations to use Num and TNum, plus occasional need to use Shared ReadOnly tn As TNum field for direct access to INumeric methods.

    The solution so far is better than the alternatives (for my criteria), but I would still like to eliminate extra "gunk" where possible.