In C# adding SelectMany extends linq to a new monad type, how do I do the same thing in VB.net?
Question
An old Yet Another Language Geek blog post explaining monads describes adding a SelectMany extension method to C# in order to extend the linq syntax to new types.
I've tried it in C# and it works. I did a straight conversion to VB.net and it doesn't work. Does anyone know if VB.net supports this feature or how to use it?
Here is the C# code which works:
class Identity<T> {
public readonly T Value;
public Identity(T value) { this.Value = value; }
}
static class MonadExtension {
public static Identity<T> ToIdentity<T>(this T value) {
return new Identity<T>(value);
}
public static Identity<V> SelectMany<T, U, V>(this Identity<T> id, Func<T, Identity<U>> k, Func<T, U, V> s) {
return s(id.Value, k(id.Value).Value).ToIdentity();
}
}
class Program {
static void Main(string[] args) {
var r = from x in 5.ToIdentity()
from y in 6.ToIdentity()
select x + y;
}
}
Here is the VB.net code which doesn't work (note: written in vs2010, so may be missing some line continuations):
Imports System.Runtime.CompilerServices
Public Class Identity(Of T)
Public ReadOnly value As T
Public Sub New(ByVal value As T)
Me.value = value
End Sub
End Class
Module MonadExtensions
<Extension()> _
Public Function ToIdentity(Of T)(ByVal value As T) As Identity(Of T)
Return New Identity(Of T)(value)
End Function
<Extension()> _
Public Function SelectMany(Of T, U, V)(ByVal id As Identity(Of T), ByVal k As Func(Of T, Identity(Of U)), ByVal s As Func(Of T, U, V)) As Identity(Of V)
Return s(id.value, k(id.value).value).ToIdentity()
End Function
End Module
Public Module MonadTest
Public Sub Main()
''Error: Expression of type 'Identity(Of Integer)' is not queryable.
Dim r = From x In 5.ToIdentity() _
From y In 6.ToIdentity() _
Select x + y
End Sub
End Module
Solution
Apparently VB.net requires that, in addition to defining SelectMany, the target type must implement the methods you want (eg. Select, Where, etc).
Add this method to Identity and the program compiles and works:
Public Function [Select](Of R)(ByVal projection As Func(Of T, R)) As Identity(Of R)
Return projection(value).ToIdentity
End Function
You can also implement it as an extension method to "linq-ify" existing types:
<Extension()> _
Public Function [Select](Of T, R)(ByVal this As Identity(Of T), ByVal projection As Func(Of T, R)) As Identity(Of R)
Return projection(this.value).ToIdentity
End Function
Also, VB.net only requires SelectMany if there are multiple 'from' lines. If the expression is of the form "from x select x+1" then only the Identity.Select method needs to be implemented.
OTHER TIPS
The equivalent code should be supported by VB as well. Make sure you're properly translating the extension methods correctly:
This C#:
public static class MonadExtensions
{
public static Identity<T> ToIdentity<T>(this T value)
{
return new Identity<T>(value);
}
}
Would become this VB:
Imports System.Runtime.CompilerServices
Module MonadExtensions
<Extension()> _
Public Function ToIdentity(Of T)(ByVal value As T) As Identity(Of T)
Return New Identity(Of T)(value)
End Function
End Module
Update: Your code above is correct, so it seems you're just running into a limitation of the VB compiler. As far as I can tell, what you're trying to do is legal according to the language spec.
However, I was able to trick the compiler into accepting the query by having Identity(Of T)
pretend to implement IEnumerable(Of T)
:
Public Class Identity(Of T)
Implements IEnumerable(Of T)
Public ReadOnly value As T
Public Sub New(ByVal value As T)
Me.value = value
End Sub
Public Function GetEnumerator() As IEnumerator(Of T) _
Implements IEnumerable(Of T).GetEnumerator
Throw New InvalidOperationException("This should never be called.")
End Function
Public Function GetEnumerator1() As IEnumerator _
Implements IEnumerable(Of T).GetEnumerator
Throw New InvalidOperationException("This should never be called.")
End Function
End Class
Once we convince the compiler that it's a valid query, it correctly resolves the call to your custom SelectMany
.
Update 2: Or yeah, what Strilanc you said. I tried that first but apparently forgot the Extension
attribute. From the language spec, something is considered queryable if, in order of preference...
- It defines a conforming Select method.
- It has an AsEnumerable() or AsQueryable() method.
- It has a Cast(Of T) method.