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:
Notice that I've changed the input argument in the first
Matchargument 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
() => xhas the typeFunc<T>, notFunc<Unit, T>. (F# is more elegant, becausefun () -> xdoes indicate a functionunit -> 'a, because in F#,()has the typeunit. Not so in C#, where 'no data' as input in a lambda expression has the special syntax() =>, and as output has the special keywordvoid. So, just to belabour the point, a C# lambda expressionx => {}has the typeAction<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 aFunc<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 aFunc<T>(not aFunc<Unit, T>)as input, and finds this one:in LanguageExt's
ListExtensions. Notice theFunc<B> Emptyparameter.This is an extension method on
IEnumerable<T>rather thanEither<L, R>. Why does this compile?Well, it compiles because
Either<L, R>implementsIEnumerable<T>: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/theUnitvalue.This now excludes the
IEnumerable<T>extension method, and instead enables the compiler to locate the correctMatchmethod onEither<L, R>.