Is it possible to leverage type inference better? #1395
-
I did a quick search and couldn't find any other similar inference questions, so my first assumption is that I'm doing something wrong below. I'm coming from scala/haskell which have pretty good type inference. Is there a way to use type inference more effectively, or perhaps a better way to write the snippet below? The C# is still pretty new to me, so it may just be language limitation. Open to any feedback!
As an aside, I also find the constructions of EitherAsync/TryAsync vs Either/Try to be a bit confusing to work with and navigate when coming from a Task context or something like that. What is generally the preferred structure to work with for error handling in an async context? Is the V5 api stable enough to look in to it? It looks promising. Higher kinds specifically help a lot with all this |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 1 reply
-
Does this code even compile? Without other context its too hard to understand you goal. Using v4, it would be easier to just implement your function with public Aff<Download<Stream>> DownloadByteStream(string url) =>
from response in Aff(async () => await client.GetAsync(url))
from _0 in guardnot<Error>(response.IsSuccessStatusCode, new ResponseFailure(url, response))
from download in Aff(async () => await Download.CreateDownloadStream(url, response))
.MapFail(e => new AttemptConvertFromException(url, e))
select download; I've really tried combinding
Read this wiki page, especially error handling part. Currently, it's still written in terms of v4, but some things are still appliable to v5.
It is very promising. Louthy's quote from recent discussion: |
Beta Was this translation helpful? Give feedback.
-
In terms of type-inference, I covered a similar question yesterday, which should give some context. A few side-notes:
In terms of going to
@aloslider pointed you in a good direction for
public Eff<Download<Stream>> DownloadByteStream(string url) =>
from response in liftEff(async () => await client.GetAsync(url))
from _ in guardnot<Error>(response.IsSuccessStatusCode, new ResponseFailure(url, response))
from download in liftEff(async () => await Download.CreateDownloadStream(url, response))
.MapFail(e => new AttemptConvertFromException(url, e))
select download; One piece of information missing from @aloslider's response was that your alternative-value error-types must derive from the in-built So, your error-types might look like this: enum AppErrorCode
{
ResponseFailure = 1,
DownloadFailure
}
record ResponseFailure(string Url, HttpResponseMessage Response)
: Expected("download response failed", (int)AppErrorCode.ResponseFailure);
record DownloadFailure(string Url, Error Except)
: Expected("download failed", (int)AppErrorCode.DownloadFailure, Except); The Finally, I like to create a public static class AppError
{
public static Error ResponseFailure(string url, HttpResponseMessage response) =>
new ResponseFailure(url, response);
public static Error DownloadFailure(string url, Error except) =>
new DownloadFailure(url, except);
} That means your final code can look like this: public Eff<Download<Stream>> DownloadByteStream(string url) =>
from response in liftEff(async () => await client.GetAsync(url))
from _ in guardnot<Error>(response.IsSuccessStatusCode, AppError.ResponseFailure(url, response))
from download in liftEff(async () => await Download.CreateDownloadStream(url, response))
.MapFail(e => AppError.DownloadFailure(url, e))
select download; If you don't want to use So, you want a method that would look like this: public EitherT<YourError, IO, Download<Stream>> DownloadByteStream(string url) =>
from response in liftIO(async () => await client.GetAsync(url))
from _ in guardnot<YourError>(response.IsSuccessStatusCode, YourError.ResponseFailure(url, response))
from download in liftIO(async () => await Download.CreateDownloadStream(url, response))
.MapFail(e => YourError.DownloadFailure(url, e))
select download; That won't work though, because IO doesn't know how to implicitly work with your bespoke error type. The That will require a new error type that will hold exceptions (the public partial record YourError;
public record ExceptError(Error Error) : YourError;
public record ResponseFailureError(string Url, HttpResponseMessage Response) : YourError;
public record DownloadFailureError(string Url, YourError Error) : YourError;
public partial record YourError
{
public static YourError Except(Error error) =>
new ExceptError(error);
public static YourError ResponseFailure(string url, HttpResponseMessage response) =>
new ResponseFailureError(url, response);
public static YourError DownloadFailure(string url, YourError except) =>
new DownloadFailureError(url, except);
}
Then your bespoke EitherT<YourError, IO, A> yourLift<A>(Func<Task<A>> f) =>
liftIO(f)
.Match(Succ: Right<YourError, A>,
Fail: e => Left<YourError, A>(YourError.Except(e))); Then your method would look like this: public EitherT<YourError, IO, Download<Stream>> DownloadByteStream(string url) =>
from response in yourLift(async () => await client.GetAsync(url))
from _ in guardnot(response.IsSuccessStatusCode, YourError.ResponseFailure(url, response))
from download in yourLift(async () => await Download.CreateDownloadStream(url, response))
.MapLeft(e => YourError.DownloadFailure(url, e))
select download;
Finally, it tends to be good to factor out any public EitherT<YourError, IO, HttpResponseMessage> GetAsync(HttpClient client, string url) =>
from response in yourLift(async () => await client.GetAsync(url))
from _ in guardnot(response.IsSuccessStatusCode, YourError.ResponseFailure(url, response))
select response;
public EitherT<YourError, IO, Download<Stream>> CreateDownloadStream(HttpResponseMessage response, string url) =>
yourLift(async () => await Download.CreateDownloadStream(url, response))
.MapLeft(e => YourError.DownloadFailure(url, e)); Then your method looks quite neat and declarative: public EitherT<YourError, IO, Download<Stream>> DownloadByteStream(string url) =>
from response in GetAsync(client, url)
from download in CreateDownloadStream(response, url)
select download; Lifting of IO is very much dealing with the messiness of the imperative world, so packaging like this works well. If you want to avoid exceptions entirely and force a conversion to a good error type, then you can remove the public partial record YourError;
public record ResponseFailureError(string Url, HttpResponseMessage Response) : YourError;
public record DownloadFailureError(string Url, YourError Error) : YourError;
public partial record YourError
{
public static YourError ResponseFailure(string url, HttpResponseMessage response) =>
new ResponseFailureError(url, response);
public static YourError DownloadFailure(string url, YourError except) =>
new DownloadFailureError(url, except);
} And force EitherT<YourError, IO, A> yourLift<A>(Func<Task<A>> f, Func<Error, YourError> mapError) =>
liftIO(f)
.Match(Succ: Right<YourError, A>,
Fail: e => Left<YourError, A>(mapError(e))); Finally finally, you should consider various domains in your application, like APIs, services, database, etc. and consider wrapping up the monad-transformer stack in a bespoke monad. So, let's say we convert public record App<A>(EitherT<YourError, IO, A> runApp) : K<App, A>
{
public App<B> Map<B>(Func<A, B> f) => this.Kind().Map(f).As();
public App<B> Select<B>(Func<A, B> f) => this.Kind().Map(f).As();
public App<A> MapFail(Func<YourError, YourError> f) => this.Kind().Catch(f).As();
public App<B> Bind<B>(Func<A, K<App, B>> f) => this.Kind().Bind(f).As();
public App<B> Bind<B>(Func<A, K<IO, B>> f) => this.Kind().Bind(f).As();
public App<C> SelectMany<B, C>(Func<A, K<App, B>> b, Func<A, B, C> p) => this.Kind().SelectMany(b, p).As();
public App<C> SelectMany<B, C>(Func<A, K<IO, B>> b, Func<A, B, C> p) => this.Kind().SelectMany(b, p).As();
public App<C> SelectMany<C>(Func<A, Guard<YourError, Unit>> b, Func<A, Unit, C> p) => SelectMany(a => App.lift(b(a)), p);
}
Then we need some extensions: public static class AppExtensions
{
public static App<A> As<A>(this K<App, A> ma) =>
(App<A>)ma;
public static IO<Either<YourError, A>> Run<A>(this K<App, A> ma) =>
ma.As().runApp.Run().As();
public static App<C> SelectMany<A, B, C>(this K<App, A> ma, Func<A, App<B>> b, Func<A, B, C> p) => ma.Kind().SelectMany(b, p).As();
public static App<C> SelectMany<A, B, C>(this K<IO, A> ma, Func<A, K<App, B>> b, Func<A, B, C> p) => ma.Kind().SelectMany(b, p).As();
public static App<C> SelectMany<B, C>(this Guard<YourError, Unit> ma, Func<Unit, K<App, B>> b, Func<Unit, B, C> p) => App.lift(ma).SelectMany(b, p);
}
Now we need to implement the traits: public partial class App :
Monad<App>,
Fallible<YourError, App>
{
static K<App, B> Monad<App>.Bind<A, B>(K<App, A> ma, Func<A, K<App, B>> f) =>
new App<B>(ma.As().runApp.Bind(x => f(x).As().runApp));
static K<App, B> Functor<App>.Map<A, B>(Func<A, B> f, K<App, A> ma) =>
new App<B>(ma.As().runApp.Map(f));
static K<App, A> Applicative<App>.Pure<A>(A value) =>
new App<A>(EitherT.Right<YourError, IO, A>(value));
static K<App, B> Applicative<App>.Apply<A, B>(K<App, Func<A, B>> mf, K<App, A> ma) =>
new App<B>(mf.As().runApp.Apply(ma.As().runApp));
static K<App, A> MonadIO<App>.LiftIO<A>(IO<A> ma) =>
new App<A>(ma.Match(Succ: Right<YourError, A>,
Fail: e => Left<YourError, A>(YourError.Except(e))));
static K<App, A> Fallible<YourError, App>.Fail<A>(YourError error) =>
new App<A>(EitherT.Left<YourError, IO, A>(error));
static K<App, A> Fallible<YourError, App>.Catch<A>(
K<App, A> fa,
Func<YourError, bool> Predicate,
Func<YourError, K<App, A>> Fail) =>
new App<A>(fa.As().runApp.Catch(Predicate, e => Fail(e).As().runApp).As());
}
Notice also the Also, notice that all of the implementations are just wrappers. They just invoke the behaviours of the Finally, we add a friendly API surface for the public partial class App
{
public static App<A> pure<A>(A value) =>
pure<App, A>(value).As();
public static App<Unit> fail(YourError error) =>
fail<YourError, App>(error).As();
public static App<A> fail<A>(YourError error) =>
fail<YourError, App, A>(error).As();
public static App<A> liftIO<A>(IO<A> computation) =>
MonadIO.liftIO<App, A>(computation).As();
public static App<A> liftIO<A>(Func<Task<A>> computation) =>
MonadIO.liftIO<App, A>(IO.liftAsync(computation)).As();
public static App<A> lift<A>(Either<YourError, A> either) =>
new App<A>(EitherT.lift<YourError, IO, A>(either)).As();
public static App<Unit> lift(Guard<YourError, Unit> guard) =>
lift(guard.ToEither());
} Obviously, you can expand that over time with more app specific features and support more of the standard operators. Now your code looks like this: public App<HttpResponseMessage> GetAsync(HttpClient client, string url) =>
from response in App.liftIO(async () => await client.GetAsync(url))
from _ in guardnot(response.IsSuccessStatusCode, YourError.ResponseFailure(url, response))
select response;
public App<Download<Stream>> CreateDownloadStream(HttpResponseMessage response, string url) =>
App.liftIO(async () => await Download.CreateDownloadStream(url, response))
.MapFail(e => YourError.DownloadFailure(url, e));
public App<Download<Stream>> DownloadByteStream(string url) =>
from response in GetAsync(client, url)
from download in CreateDownloadStream(response, url)
select download; Which might seem like a lot of code to get not much change. However, you should only need to do this a few times depending on your application. Maybe an The key benefit to doing this is that you can change your mind. It's refactor friendly... You're using an So, we can change just the First, let's create an public record AppEnv(Option<HttpClient> Client); Now modify the public record App<A>(ReaderT<AppEnv, EitherT<YourError, IO>, A> runApp) : K<App, A>
{
public App<B> Map<B>(Func<A, B> f) => this.Kind().Map(f).As();
public App<B> Select<B>(Func<A, B> f) => this.Kind().Map(f).As();
public App<A> MapFail(Func<YourError, YourError> f) => this.Kind().Catch(f).As();
public App<B> Bind<B>(Func<A, K<App, B>> f) => this.Kind().Bind(f).As();
public App<B> Bind<B>(Func<A, K<IO, B>> f) => this.Kind().Bind(f).As();
public App<C> SelectMany<B, C>(Func<A, K<App, B>> b, Func<A, B, C> p) => this.Kind().SelectMany(b, p).As();
public App<C> SelectMany<B, C>(Func<A, K<IO, B>> b, Func<A, B, C> p) => this.Kind().SelectMany(b, p).As();
public App<C> SelectMany<C>(Func<A, Guard<YourError, Unit>> b, Func<A, Unit, C> p) => SelectMany(a => App.lift(b(a)), p);
}
public static class AppExtensions
{
public static App<A> As<A>(this K<App, A> ma) =>
(App<A>)ma;
public static IO<Either<YourError, A>> Run<A>(this K<App, A> ma, AppEnv config) =>
ma.As().runApp.Run(config).Run().As();
public static App<C> SelectMany<A, B, C>(this K<App, A> ma, Func<A, App<B>> b, Func<A, B, C> p) => ma.Kind().SelectMany(b, p).As();
public static App<C> SelectMany<A, B, C>(this K<IO, A> ma, Func<A, K<App, B>> b, Func<A, B, C> p) => ma.Kind().SelectMany(b, p).As();
public static App<C> SelectMany<B, C>(this Guard<YourError, Unit> ma, Func<Unit, K<App, B>> b, Func<Unit, B, C> p) => App.lift(ma).SelectMany(b, p);
}
public partial class App :
Monad<App>,
Fallible<YourError, App>,
Readable<App, AppEnv>
{
static K<App, B> Monad<App>.Bind<A, B>(K<App, A> ma, Func<A, K<App, B>> f) =>
new App<B>(ma.As().runApp.Bind(x => f(x).As().runApp));
static K<App, B> Functor<App>.Map<A, B>(Func<A, B> f, K<App, A> ma) =>
new App<B>(ma.As().runApp.Map(f));
static K<App, A> Applicative<App>.Pure<A>(A value) =>
new App<A>(ReaderT.lift<AppEnv, EitherT<YourError, IO>, A>(EitherT.Right<YourError, IO, A>(value)));
static K<App, B> Applicative<App>.Apply<A, B>(K<App, Func<A, B>> mf, K<App, A> ma) =>
new App<B>(mf.As().runApp.Apply(ma.As().runApp));
static K<App, A> MonadIO<App>.LiftIO<A>(IO<A> ma) =>
new App<A>(
ReaderT.lift<AppEnv, EitherT<YourError, IO>, A>(
EitherT.liftIO<YourError, IO, A>(
ma.Match(Succ: Right<YourError, A>,
Fail: e => Left<YourError, A>(YourError.Except(e))))));
static K<App, A> Fallible<YourError, App>.Fail<A>(YourError error) =>
new App<A>(ReaderT.lift<AppEnv, EitherT<YourError, IO>, A>(EitherT.Left<YourError, IO, A>(error)));
static K<App, A> Fallible<YourError, App>.Catch<A>(
K<App, A> fa,
Func<YourError, bool> Predicate,
Func<YourError, K<App, A>> Fail) =>
new App<A>(
new ReaderT<AppEnv, EitherT<YourError, IO>, A>(
env => fa.As().runApp.runReader(env).Catch(Predicate, e => Fail(e).As().runApp.runReader(env)).As()));
static K<App, A> Readable<App, AppEnv>.Asks<A>(Func<AppEnv, A> f) =>
new App<A>(ReaderT.asks<EitherT<YourError, IO>, A, AppEnv>(f));
static K<App, A> Readable<App, AppEnv>.Local<A>(Func<AppEnv, AppEnv> f, K<App, A> ma) =>
new App<A>(ReaderT.local(f, ma.As().runApp));
}
public partial class App
{
public static App<A> pure<A>(A value) =>
pure<App, A>(value).As();
public static App<Unit> fail(YourError error) =>
fail<YourError, App>(error).As();
public static App<A> fail<A>(YourError error) =>
fail<YourError, App, A>(error).As();
public static App<A> liftIO<A>(IO<A> computation) =>
MonadIO.liftIO<App, A>(computation).As();
public static App<A> liftIO<A>(Func<Task<A>> computation) =>
MonadIO.liftIO<App, A>(IO.liftAsync(computation)).As();
public static App<A> lift<A>(Either<YourError, A> either) =>
lift(EitherT.lift<YourError, IO, A>(either)).As();
public static App<A> lift<A>(EitherT<YourError, IO, A> eitherT) =>
new App<A>(ReaderT.lift<AppEnv, EitherT<YourError, IO>, A>(eitherT)).As();
public static App<Unit> lift(Guard<YourError, Unit> guard) =>
lift(guard.ToEither());
} Not everything needed changing, so I'll leave it to you to look for the changes to support the This all stays the same though: public App<HttpResponseMessage> GetAsync(HttpClient client, string url) =>
from response in App.liftIO(async () => await client.GetAsync(url))
from _ in guardnot(response.IsSuccessStatusCode, YourError.ResponseFailure(url, response))
select response;
public App<Download<IO.Stream>> CreateDownloadStream(HttpResponseMessage response, string url) =>
App.liftIO(async () => await Download.CreateDownloadStream(url, response))
.MapFail(e => YourError.DownloadFailure(url, e));
public App<Download<Stream>> DownloadByteStream(string url) =>
from response in GetAsync(client, url)
from download in CreateDownloadStream(response, url)
select download; So, a major refactor didn't require a change throughout the code-base. If we now add some new error types: public record HttpClientAlreadySet : YourError;
public record HttpClientNotSet : YourError; And add a couple of functions to the public static App<AppEnv> ask =>
Readable.ask<App, AppEnv>().As();
public static App<A> asks<A>(Func<AppEnv, A> f) =>
Readable.asks<App, AppEnv, A>(f).As();
public static App<A> local<A>(Func<AppEnv, AppEnv> f, App<A> ma) =>
Readable.local(f, ma).As();
public static App<A> withHttpClient<A>(App<A> ma) =>
from c in asks(e => e.Client)
from _ in when(c.IsSome, fail(YourError.HttpClientAlreadySet))
from r in local(e => e with { Client = new HttpClient() }, ma)
select r;
public static App<HttpClient> httpClient =>
from c in asks(e => e.Client)
from _ in when(c.IsNone, fail(YourError.HttpClientNotSet))
select (HttpClient)c; The first three are to access the underlying Now you can make functions So, public static App<HttpResponseMessage> GetAsync(string url) =>
from client in App.httpClient
from response in App.liftIO(async () => await client.GetAsync(url))
from _ in guardnot(response.IsSuccessStatusCode, YourError.ResponseFailure(url, response))
select response; And your original function becomes even simpler: public static App<Download<Stream>> DownloadByteStream(string url) =>
from response in GetAsync(url)
from download in CreateDownloadStream(response, url)
select download; Everything it uses is from its parameters, not some potentially invalid/mutable state in a class. And because you should only create one So yeah, it's possible to get into generics hell with this library if you start down the path of trying to mix and match too many concepts. But, if you focus in on exactly what effects you're trying to capture, then package them up into domain-specific monads, things start to become quite elegant. An example I keep pulling out is the CardGame sample app. It does a similar thing to what I wrote above. It wraps a Anyway, long post, but I hope that helps! |
Beta Was this translation helpful? Give feedback.
In terms of type-inference, I covered a similar question yesterday, which should give some context.
A few side-notes:
Prelude.Right
, you should use the static-using:using static LanguageExt.Prelude
, then you can just writeRight
.In terms of going to
v5
, yes, I would advise that for anyone starting with language-ext. There are a few outstanding issues, but they're listed on the issues page and quite small overall. The only only issue you might have withv5
are: