2

I am learning FP with language-ext and I ran into a problem that I have not been able to overcome. I simplified my code down to this example:

using System;
using System.Threading.Tasks;
using LanguageExt;
using static LanguageExt.Prelude;
using Xunit;

namespace Temp {

    public class SelectManyError {

        [Fact]
        public async Task Do() {

            var six = await from a in Task.FromResult(Right<Exception, int>(1))
                            from b in Task.FromResult(Right<Exception, int>(2))
                            from c in Task.FromResult(Right<Exception, int>(3))
                            select a + b + c;

        }
    }
}

I am getting this error:

Multiple implementations of the query pattern were found for source type Task<Either<Exception, int>>. Ambiguous call to 'SelectMany'.

I understand what the compiler thinks the issue is from reading this webpage. But, I am clearly missing or not understanding something important because I cannot figure out how this error is caused by this scenario OR what to do about it. This will work just fine if it is only 2 from clauses, which confuses me even more.

Is this the wrong approach to this type of problem? Is there another way I am unaware of?

nuester
  • 23
  • 6

3 Answers3

4

Lang-ext author here. We've been discussing the issue on the lang-ext github repo.

These are my comments:

It's tough. They're not really false positives to be honest, because Either<L, R> supports the + operator, and so the SelectMany that belongs to Task<R> will produce a valid result just like the SelectMany that works with Task<Either<L, R>>.

Basically the a, b, and c values could legitimately be int or Either<Exception, int> depending on which SelectMany implementation the compiler chooses.

The whole expression is valid for all SelectMany extensions, which is obviously why we have that ambiguity.

It's a shame that changing var three = ... to Either<Exception, int> three = ... doesn't change the inference system. Because that's the key difference between the two possible expressions that the compiler is confused by.

One thing you might want to do instead of using Task<Option<A>> is use OptionAsync<A> and instead of Task<Either<L, R>> use EitherAsync<L, R>. They're essentially exactly the same types, except it's got all the binding semantics wrapped up nicely so you'll never have this issue again.

I am going through the process of creating an *Async variant for all the monadic types in lang-ext. For convenience, potential performance benefits, and to allow the equivalent of 3 nested levels of monads: M<A<B<C>>> for example a Seq<OptionAsync<A>> is the same as Seq<Task<Option<A>>>.

Anyway, back to your example above, you could instead do:

public async Task<int> Method()
{
    var six = from a in Right<Exception, int>(1).ToAsync()
              from b in Right<Exception, int>(2).ToAsync()
              from c in Right<Exception, int>(3).ToAsync()
              select a + b + c;

    return await six.IfLeft(0);
}

Or if you want to construct from a Task<int>:

public async Task<int> Method()
{
    var six = from a in RightAsync<Exception, int>(Task.FromResult(1))
              from b in RightAsync<Exception, int>(Task.FromResult(2))
              from c in RightAsync<Exception, int>(Task.FromResult(3))
              select a + b + c;

     return await six.IfLeft(0);
}

Or, you could stay inside the monad and return that:

public EitherAsync<Exception, int> Method() =>
    from a in RightAsync<Exception, int>(Task.FromResult(1))
    from b in RightAsync<Exception, int>(Task.FromResult(2))
    from c in RightAsync<Exception, int>(Task.FromResult(3))
    select a + b + c;
louthster
  • 1,560
  • 9
  • 20
2

The compiler is having a hard time understanding what the type of a is supposed to be (either int or Either<Exception, int>) since it is unused on the second from line.

Here is a awfully ugly workaround for this specific case. However, for any type, I think the hack can be adapted to work for that type.

using System;
using System.Threading.Tasks;
using LanguageExt;
using Xunit;
using static LanguageExt.Prelude;

public class Namespace
{
    [Fact]
    public async Task Method()
    {
        var six = await from a in Right<Exception, int>(1).AsTask()
                        from b in Right<Exception, int>(a - a + 2).AsTask()
                        from c in Right<Exception, int>(3).AsTask()
                        select a + b + c;
    }
}
Tyson Williams
  • 1,630
  • 15
  • 35
0

Another way to work around this is by using how the compiler searches for matching extension methods.

From the C# spec

The search [..] proceeds as follows:

Starting with the closest enclosing namespace declaration, continuing with each enclosing namespace declaration, and ending with the containing compilation unit, successive attempts are made to find a candidate set of extension methods:

If the given namespace or compilation unit directly contains non-generic type declarations Cᵢ with eligible extension methods Mₑ, then the set of those extension methods is the candidate set.

If namespaces imported by using namespace directives in the given namespace or compilation unit directly contain non-generic type declarations Cᵢ with eligible extension methods Mₑ, then the set of those extension methods is the candidate set.

So .. if you add your own version of the extension methods needed for the LINQ query syntax to work (Select and SelectMany) within your application, at the same or a higher level in the namespace hierarchy as the calling code, these will be used and the two ambiguous versions in the LanguageExt namespace will never be considered.

Your extensions can just delegate to the generated source code in LanguageExt.Transformers.

Here I'm using Task<Validation< rather than Task<Either<; just check the source-code for the extensions class name of the particular combination of stacked monads you are using:

using System;
using System.Threading.Tasks;
using LanguageExt;

namespace YourApplication;

public static class BindDisambiguationExtensions
{
    public static Task<Validation<FAIL, B>> Select<FAIL, A, B>(
        this Task<Validation<FAIL, A>> ma,
        Func<A, B> f) =>
        ValidationT_AsyncSync_Extensions.Select(ma, f);

    public static Task<Validation<FAIL, C>> SelectMany<FAIL, A, B, C>(
        this Task<Validation<FAIL, A>> ma,
        Func<A, Task<Validation<FAIL, B>>> bind,
        Func<A, B, C> project) =>
        ValidationT_AsyncSync_Extensions.SelectMany(ma, bind, project);
}
Appetere
  • 6,003
  • 7
  • 35
  • 46