louthy / language-ext

C# functional language extensions - a base class library for functional programming
MIT License
6.48k stars 417 forks source link

TryAsyncExtensions.BiMap is actually a Fold, not a BiMap #563

Closed doomchild closed 5 years ago

doomchild commented 5 years ago

The two functions passed to TryAsync.BiMap are A -> B and Exception -> B. This is a fold or a reduce, not a bimap. I ran into this when trying to write a Tap function for TryAsync, which I would normally write like this:

public static TryAsync<TResult> Tap<TResult>(this TryAsync<TResult> tryAsync, Action<TResult> successAction, Action<Exception> failureAction)
{
  return tryAsync.BiMap(successValue =>
  {
    successAction(successValue);
    return successValue;
  },
  exception =>
  {
    failureAction(exception);
    return exception;
  }
}

This is obviously not possible if both sides of BiMap must resolve to the same type.

I'm aware that this is introducing a side effect, and that it's not necessarily good functional style. But sometimes you can't ignore a side effect (logging, for example, is entirely side-effecty), and a function like this is a whole lot easier to write and maintain on a team without a lot of functional grounding than a full State/Reader/Writer setup.

I think TryAsyncExtensions.BiMap should either be renamed to Fold if the behavior is intended, or changed to allow a proper bimap to happen.

louthy commented 5 years ago

Disagree. TryAsync<A> is essentially Either<Task<Exception>, Task<A>>. Just the left must always be an Exception. The two sides are still mappable, but the resulting type of the left-hand-side must always be the same, even if the value has been mapped. So, it will stay as BiMap.

doomchild commented 5 years ago

How is that a BiMap at all, though?

Maybe I'm leaning too hard on the Haskell idea of what these concepts mean, but bimap :: (a -> b) -> (c -> d) -> p a c -> p b d (from https://hackage.haskell.org/package/bifunctors-5/docs/Data-Bifunctor.html) tells me that after a bimap has occurred, the left and right sides do not have to be the same type. TryAsync.BiMap is requiring both sides to turn into the same type. I would expect the signature to be BiMap<TResult>(Func<A, TResult> rightMorphism, Func<Exception, Exception> leftMorphisM).

TysonMN commented 5 years ago

I think of Fold as being behavior specific to the case of having (potentially) multiple instances of a single thing (i.e. IEnumberable<> or Seq<>).

Instead, the first thought that comes to my mind would be to rename this TryAsync.BiMap to TryAsync.Match. That seems consistent to me with the Match method for Either<L, R>, which is

public Ret Match<Ret>(Func<R, Ret> Right, Func<L, Ret> Left, Func<Ret> Bottom = null)
doomchild commented 5 years ago

Yeah, you could call it Match, and that would make sense, because all of the other Match methods coerce both sides of the disjunction into the same type.