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.
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.