views:

399

answers:

2

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
+2  A: 

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

  1. It defines a conforming Select method.
  2. It has an AsEnumerable() or AsQueryable() method.
  3. It has a Cast(Of T) method.
dahlbyk
I've included sample code in the question now, so you can check if I've translated correctly.
Strilanc
+2  A: 

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.

Strilanc