Map and Apply standardisation
Pre-releaseNow that language-ext has Functor
and Applicative
traits, there are a number of extension methods[1][2] and module functions[3][4] that work with those traits.
Those all return the abstract K<F, A>
type rather than the more specialised types that derive from K<F, A>
. So, to get over that, I had previously added bespoke extensions for types like Either
, Option
, etc. that would call the generic behaviours and then cast back to the specialised type.
Unfortunately, over the years there's been an inconsistent application of these extension methods to the various functor/applicative types. So, I have now made all functors/applicatives support the exact same set of Map
, Apply
, Action
extensions as well as the exact same set of map
, apply
, action
functions in the Prelude
.
That means for some types you may have lost some extensions/functions and for some they have gained. But, they are now all consistent, so going forward at least there's no second guessing.
One big change is that the multiple operand Apply
has gone, so you can't do this now:
var mf = Some((int x, int y) => x + y);
var mx = Some(100);
var my = Some(100);
var mr = mf.Apply(mx, my);
You must fluently chain calls to Apply
(which is just what it did behind the scenes anyway):
var mr = mf.Apply(mx).Apply(my);
The variants of Map
and Apply
that took multi-argument Func
delegates as their first argument are now all only available as generic extensions and only accept a single operand, e.g:
public static K<AF, Func<B,Func<C, D>>> Apply<AF, A, B, C, D>(
this K<AF, Func<A, B, C, D>> mf,
K<AF, A> ma)
where AF : Applicative<AF> =>
AF.Apply(AF.Map(curry, mf), ma);
public static K<AF, Func<B,Func<C, Func<D, E>>>> Apply<AF, A, B, C, D, E>(
this K<AF, Func<A, B, C, D, E>> mf,
K<AF, A> ma)
where AF : Applicative<AF> =>
AF.Apply(AF.Map(curry, mf), ma);
// ... etc. up to 10 parameter Func delegates
Note, how the multi-parameter Func
delegates turn into single parameter curried Func
results.
What that means is each bespoke extension method (say, for Option
, like below) just needs to handle Func<A, B>
and not all variants of n-argument function. All chained Apply
calls will eventually bake down to a concrete type being returned, removing the need to call .As()
afterwards.
public static Option<B> Apply<A, B>(this Option<Func<A, B>> mf, K<Option, A> ma) =>
Applicative.apply(mf, ma).As();
public static Option<B> Apply<A, B>(this K<Option, Func<A, B>> mf, K<Option, A> ma) =>
Applicative.apply(mf, ma).As();
The Prelude
now has map
, apply
, and action
functions for all applicatives. I think it's worth pointing out that map
is particularly useful in making the use of applicatives a bit easier. Previously, if you needed to lift up an anonymous lambda, you'd need to call fun(x => ...)
to make the delegate available:
var mr = fun((int x, int y) => x + y)
.Map(mx)
.Apply(my);
Now, with the map
overrides, you can avoid the initial lifting of the function and perform the lift and map all in one:
var mr = map((int x, int y) => x + y, mx)
.Apply(my);
Of course, the tuple-based approach is also available for all applicatives:
var mr = (mx, my).Apply((x, y) => x + y);
This however returns the generic K<F, A>
and needs .As()
to make it concrete.