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