Search code examples
c#functional-programminglanguage-ext

Why is my Either-returning method always returning Left, irrespective of what happened?


The use case here is that I have added a method to an Entity Framework DbContext that does some extra work before saving, and then returns an Either depending on the results. A very simplified version of this looks like this...

static async Task<Either<string, Unit>> SaveChangesAsyncEither(string userId) {
  // In reality, this would do some auditing, check for concurrency issues, and
  // attempt to save the changes. It would return unit if all went OK, 
  // or an error message if not. Simple check for demonstrating the issue...
  if (userId == "jim") {
    return "We don't like Jim";
  }
  return unit;
}

This method is used in many places in my solution, and wors fine.

In one minimal API project, I have a method that looks like this (again, highly simplified)...

static async Task<Either<DeviceApiResponseStates, DeviceApiResponseStates>> CreateDevice(string userId) {
  // In reality we would have more parameters, create a device, etc before
  // calling SaveChangesAsyncEither
  return (await SaveChangesAsyncEither(userId))
      .Match(() => Right<DeviceApiResponseStates, DeviceApiResponseStates>(DeviceApiResponseStates.OK),
        _ => Left(DeviceApiResponseStates.Nah));
}

...where DeviceApiResponseStates is an enum. For simplicity, you can imagine it looks like this...

enum DeviceApiResponseStates {
  OK,
  Nah
}

Those three code blocks are a complete sample of my problem.

I would expect that if I call CreateDevice("jim"), then I would get a Left with a value of Nah, and if I call it with any other string value, I would get a Right with a value of OK.

However, when I try this...

Console.WriteLine((await CreateDevice(""))
  .Match(_ => "Yeah", ex => $"Exception: {ex}"));

... I get Nah, irrespective of the string value.

Anyone able to explain what I'm doing wrong?


Solution

  • TL;DR: Three-keystroke edit:

    static async Task<Either<DeviceApiResponseStates, DeviceApiResponseStates>> CreateDevice(string userId)
    {
        // In reality we would have more parameters, create a device, etc before
        // calling SaveChangesAsyncEither
        return (await SaveChangesAsyncEither(userId))
            .Match(_ => Right<DeviceApiResponseStates, DeviceApiResponseStates>(DeviceApiResponseStates.OK),
                _ => Left(DeviceApiResponseStates.Nah));
    }
    

    Notice that I've changed the input argument in the first Match argument from () to _.

    ...but why?!

    Indeed, this took me some time figuring out.

    It may be important to state the following up-front, since I don't know if you're familiar with other functional languages - especially F#, but also Haskell:

    In C#, the lambda expression () => x has the type Func<T>, not Func<Unit, T>. (F# is more elegant, because fun () -> x does indicate a function unit -> 'a, because in F#, () has the type unit. Not so in C#, where 'no data' as input in a lambda expression has the special syntax () =>, and as output has the special keyword void. So, just to belabour the point, a C# lambda expression x => {} has the type Action<T>, whereas in F# fun x -> () would have the type 'a -> unit.

    All this means that when you write .Match(() => ... the C# compiler interprets that lambda expression as a Func<Either<DeviceApiResponseStates, DeviceApiResponseStates>>, that is, a function that takes no input. The C# overload resolver goes looking for a method or extension method that takes a Func<T> (not a Func<Unit, T>) as input, and finds this one:

    public static B Match<A, B>(this IEnumerable<A> list, Func<B> Empty, Func<Seq<A>, B> More)
    {
        return Prelude.toSeq(list).Match(Empty, More);
    }
    

    in LanguageExt's ListExtensions. Notice the Func<B> Empty parameter.

    This is an extension method on IEnumerable<T> rather than Either<L, R>. Why does this compile?

    Well, it compiles because Either<L, R> implements IEnumerable<T>:

    public readonly struct Either<L, R> : IEnumerable<EitherData<L, R>>, IEnumerable, ...
    

    By using _ instead of (), you tell the compiler that you expect some value as input to your lambda expression, instead of no input. It just so happens that the input you expect is a/the Unit value.

    This now excludes the IEnumerable<T> extension method, and instead enables the compiler to locate the correct Match method on Either<L, R>.