Can I make getFooBar require the Member (Error Text) constraint, such that it doesn't have to return Either? Or is this an anti-pattern for polysemy? If so, what would be the recommended approach?
I personally prefer to use the Error effect and not be specific about the returning type being able to "fail". I start by being explicit tho and then refine the signatures I don't see any need to be explicit
I have been wondering about this too. In a work I have in progress I have left the error constraint out of the API but have it in the interpreter. This way you could have an interpreter in which getFooBar always returns a Bar, and never throws an error. But it also means that you still have to handle the errors even if you only use API functions that don't throw errors.
I use Either in all effect APIs and call fromEither on every use. So Error is only used in the intermediate machinery, converting to/from Either on the interpreter boundaries. It would be really nice to have something to simplify that.
but all of this is indicative of the main problem: the error is inherently specific to an interpreter, so exposing it as an Either signature is already premature.
this was going to be my original response too - there's no need to put it in the effect itself, but then I realised there might be some effects that always have the inherent possibility to fail, and handling their failure is also a part of your "business logic" (?)
exactly, I guess there is a substantial conceptual difference between using Error in a oneshot CLI app where you would always terminate the whole process on error and something daemonic like a web app.
You could argue that this is inherently an abuse of Error, I guess
so what you would need is the Polysemy machinery checking whether an Error is caught somewhere in the other interpreters (or the main program), though that would need them to know about the errors again. you could do something like making all errors that are interpreter-specific into Text and catching that as a NonFatal X where X is the effect whose interpreter throws
could be an effect CatchNonFatal you use in the consumer that is interpreted as interpretCNF :: Sem (CatchNonFatal X : NonFatal X InterpreterError : Error InterpreterError : r) a-> Sem r a
if the consumer needs a data type for the error, then it's obviously something non-specific and may be part of the API. I do sometimes use data type errors specific to an interpreter to decide on a consequence in a consumer, so this would enforce some discipline in that area
If the exception is inherently part of the effect, so that you must have the user or want to enforce the user to deal with the exception, then yes, this is a good way to go about it.
An alternative way is to instead do the conversion at the interpreter level.
Take the example of, say, a REST api that you can do for something stateful that might fail:
That way, users can use get of the State RestfulState effect, and have it raise an exception, without the Error RestfulException effect being in scope.
You want the ability to deal with the exception in application code? No problem. Just have an Member (Error RestfulException) r at the place where you want to deal with the exception, and then use catch. Notably, you can do this without needing Member (Embed IO) r like the interpreter does.
There is a problem however: you can't, out-of-the-box, have more granular exceptions. Say you want to deal with aRestfulException generated by put and get, but in a granular way: in terms of an Error effect that's smaller than Error RestfulException, or maybe not even connected to Rest at all: say,Error PutException. If so, then you need an interpreter later down the line to convert Error PutException to Error RestfulException. You might reach for mapError, but NOPE, it will actually break due to #225. I hadn't actually realized this until now. The example in #225 is contrived in comparison.
Which I am now convinced should get added to Polysemy v2.0, probably together with a deprecation of mapError. For the moment, stick that into your code, and use it to convert between your Errors.
This way, you can get:
1. The choice to be ignorant to the fact that an action of an effect may cause an exception generated by the interpreter for that effect
2. The ability to handle exceptions in a granular way when you do care about it
mapError currently uses runError internally when processing catch, and then converting the error to the mapped error once the handling has completed. This has the benefit of mapError only requiring...
Actually, now when I think about it, sometimes conversions only go one way. For example, it might make sense to convert RestfulException to Maybe PutException, but not PutException to RestfulException. In that case, what you really want is an Error effect without throw to use in application code, à-la fused-effects. That's something we'd need to think more about.
my concern with catch is that the compiler won't notify me that I forgot to handle the interpreter's error in my program since using catch doesn't remove the need for the Error interpreter after the throwing interpreter. Obviously this isn't exactly canonical for Error, but it's what happens to me a lot. I was wondering if I'm just doing things wrong.
Hmm. Then what about combining the indirection approach with a pseudo-action that you can use in place of catch that does give you that property, like this:
dataFooAPImwhereGetFooBar'::Text->FooAPIm(EitherTexta)getFooBar::Members'[FooAPI, Error Text]r=>Text->SemrBargetFooBarstr=getFooBar'str>>=fromEither-- Compiles:one::Members'[FooAPI r, Error Text]=>Semr(Text,Text)one=dot1<-getFooBart2<-getFooBarreturn(t1,t2)two::Members'[FooAPI r, Error Text]=>Semr(Text,Text)two=dot1<-getFooBar`catch'`\e->return$"Bad1: "<>et2<-getFooBarreturn(t1,t2)usingTwo::MemberFooApir=>SemrTextusingTwo=(`catch'`\e->return$"Bad12:"<>e)$do(t1,t2)<-tworeturn$t1<>t2three::MemberFooAPIr=>Semr(Text,Text)three=dot1<-getFooBar`catch'`\e->return$"Bad1: "<>et2<-getFooBar`catch'`\e->return$"Bad2: "<>ereturn(t1,t2)-- Doesn't compile due to the second use of getFooBar:bad::MemberFooAPIr=>Semr(Text,Text)bad=dot1<-getFooBar`catch'`\e->return$"Bad1: "<>et2<-getFooBarreturn(t1,t2)
Hey, isn't this a trick some first-order extensible effects library used to get pseudo-error? Since with this, you don't need an higher-order effect for catch; you can use the pseudo-action instead.
There are probably some issues with this: I think it messes up local/global state semantics, since you're effectively taking a global "Error" effect and making it local. I'd need to look into it some more.
With catch', the error constraint is visible as long as there exists some getFooBar the possible exception of which hasn't been handled.
Compare with three and usingTwo; they don't have the constraint.
but it is also an error specific to interpretA and might not be thrown by a different interpreter, and moreover the different one might throw something else
so my earlier suggestion was to have an additional AError Text that is used as a something went wrong in A marker, which is then caught in prog and converted from E at the interpretation point
Going back to the FooAPI example, replacing Error Text with Error SmallExc.
dataFooAPImwhereGetFooBar::Text->FooAPImamakeSem''FooAPIdataFooAPIManagercemawhereManageFooAPI::forallcera.Members'[FooAPIManager c e, Error e]r=>Sem(FooAPI':r)a->FooAPIManagerce(Semr)aManageFooAPICarrier::forallcema.ca->FooAPIManagercemamanageFooAPI::forallcera.(Memberer,Member(FooAPIManagercer)=>Sem(FooAPI':r)a->SemramanageFooAPI=send.ManageFooAPI@c@e-- Some progam that makes use of getFooBar without knowing or caring that it might cause exceptionsprog'::MemberFooAPIr=>Semr()prog'=getFooBar"hiho"-- Running that piece of the program in the context of the larger application.prog::Member(FooAPIManagercSmallExc)r=>Semr()prog=catch'(manageFooAPI@c@SmallExcprog')(\_->pure())-- The primitive behind getFooBargetFooBarIO::IO(EitherBigExca)getFooBarIO=...bigToSmall::BigExc->MaybeSmallExcbigToSmall=...runFooAPIManager::Members'[Embed IO r, Error BigExc]r=>InterpreterFor(FooAPIManager(Semr)SmallExc':r)runFooAPIManager=interpretH$\caseManageFooAPIm->dom'<-runT$(`interpret`m)$\(GetFooBarexc)->doe<-send$ManageFooAPICarrier$tryJustbigToSmall(fromEitherMgetFooBarIO)fromEithereraise(runFooAPIManagerm')ManageFooAPICarrierc->raisec>>=pureT
This is terrible, but it works, somehow. FooAPIManager is the "link", here. You can use getFooBarin a part of the program without knowing it throws exceptions, but eventually, in order to run that part, you must use manageFooAPI to convert it, at which point any exception makes itself known, and you must handle that exception by using catch'. FooApiManager has an additional type variable for the carrier, which is needed to actually interpret GetFooBar with the parametrized Sem r0 seen inside ManageFooAPI, since we don't know anything about it other than it carries FooAPIManager (Sem r) SmallExc and Error SmallExc. By keeping that type variable polymorphic inside user code, you keep user code from doing naughty things with it.
I also show here how you can do this in a way that exposes a smaller, granular exception to the user code rather than the bigger exception that the primitive may generate.
This is complicated, evil, and very, very ugly. I'll try to find some way to improve it.
mapError currently uses runError internally when processing catch, and then converting the error to the mapped error once the handling has completed. This has the benefit of mapError only requiring...
Oh, sorry. It's probably becauseintNF converts its argument as a whole, rather than individual throws. Same problem as with mapError. That's also fixed by defining it in terms of errorToError.
importPolysemyimportPolysemy.InternalimportPolysemy.Internal.UnionimportPolysemy.ErrordataExceptionalerreffmawhereExceptionally::forallerreffr0a.Weavingeff(Semr0)a->Exceptionalerreff(Semr0)(Eithererra)-- Run a Sem using actions of `eff`if you're prepared to deal with any exception that may arise from those actions.exceptionally::forallerreffra.Members'[Exceptional err eff, Error err]r=>Sem(eff':r)a->Semraexceptionallysem=Sem$\k->runSemsem$\u->casedecomp(hoist(exceptionally@err)u)ofRightwav->dorunSem(send(Exceptionally@errwav)>>=fromEither)kLeftg->kg-- Runs `Exceptional exc eff` if `eff`is in the effect stack, given a way to convert from a larger exception to `exc`.runExceptional::foralleffsmallExcbigExcra.Members'[eff, Error bigExc]r=>(bigExc->MaybesmallExc)->Sem(ExceptionalsmallExceff':r)a->SemrarunExceptionalfromsem=Sem$\k->runSemsem$\u->casedecomp(hoist(runExceptionalfrom)u)ofRight(Weaving(Exceptionallywav)swvexins)->letmain=liftSem$weaveswvins(injWeavingwav)in(`runSem`k)$catchJustfrom(fmap(ex.fmapRight)main)$\e->pure(ex(Lefte<$s))Leftg->kghandle::Sem(Errore':r)a->(e->Semra)->Semrahandlemh=runErrorm>>=eitherhpure-- Boilerplate ends here.dataFooAPImawhereGetFooBar::String->FooAPImamakeSem''FooAPIdataBigExcdataSmallExc-- Some progam that makes use of getFooBar without knowing or caring that it might cause exceptionsprog'::MemberFooAPIr=>Semr()prog'=getFooBar"hiho"-- Running that piece of the program in the context of the larger application.prog::Member(ExceptionalSmallExcFooAPI)r=>Semr()prog=handle@SmallExc(exceptionally@SmallExc@FooAPIprog')(\_->pure())-- The primitive behind getFooBargetFooBarIO::String->IO(EitherBigExca)getFooBarIO=undefinedbigToSmall::BigExc->MaybeSmallExcbigToSmall=undefinedrunFooAPI::Members'[Embed IO, Error BigExc]r=>InterpreterForFooAPIrrunFooAPI=interpret$\(GetFooBarstr)->fromEitherM(getFooBarIOstr)runFooAPIExceptional::Members'[Embed IO, Error BigExc]r=>InterpreterFor(ExceptionalSmallExcFooAPI)rrunFooAPIExceptional=runFooAPI.runExceptionalbigToSmall.raiseUnder
The key combinator of interest here is exceptionally, as well as the interpreter runExceptional.
This has the benefit of shuffling more responsibility from the user to predefined functions. You don't need to understand how it works to use it!
Yeah, it uses a lot of internal stuff, but that's fine. We'll probably add Exceptional, or some generalization thereof.
Also fun fact the only reason Exceptional instantiates its parameter is because weave has a constraint it doesn't need. We'll fix that.
nice, I'll have to try that out in the wild to assess the ergonomics.
For the record, this is what I bruteforced together to achieve the UX I was going for:
So I have this effect:
Can I make
getFooBar
require theMember (Error Text)
constraint, such that it doesn't have to return Either? Or is this an anti-pattern for polysemy? If so, what would be the recommended approach?that also has been my most substantial coherence concern so far
haven't done any meaningful research though
you can catch an error blindly from a consumer, but that's not very reasonable
I personally prefer to use the Error effect and not be specific about the returning type being able to "fail". I start by being explicit tho and then refine the signatures I don't see any need to be explicit
I have been wondering about this too. In a work I have in progress I have left the error constraint out of the API but have it in the interpreter. This way you could have an interpreter in which
getFooBar
always returns aBar
, and never throws an error. But it also means that you still have to handle the errors even if you only use API functions that don't throw errors.I use
Either
in all effect APIs and callfromEither
on every use. SoError
is only used in the intermediate machinery, converting to/fromEither
on the interpreter boundaries. It would be really nice to have something to simplify that.my preference tbh would be to build the error type into
Sem
like
zio
does over in scala worlddon't know any specifics there tho
probably also complicated to make this flexible, i.e. keep a handled error from propagating in the signature
one possibility would be a TH constructor that automates the
fromEither
callbut all of this is indicative of the main problem: the error is inherently specific to an interpreter, so exposing it as an
Either
signature is already premature.this was going to be my original response too - there's no need to put it in the effect itself, but then I realised there might be some effects that always have the inherent possibility to fail, and handling their failure is also a part of your "business logic" (?)
Like API errors
exactly, I guess there is a substantial conceptual difference between using
Error
in a oneshot CLI app where you would always terminate the whole process on error and something daemonic like a web app.You could argue that this is inherently an abuse of
Error
, I guessso what you would need is the Polysemy machinery checking whether an Error is caught somewhere in the other interpreters (or the main program), though that would need them to know about the errors again. you could do something like making all errors that are interpreter-specific into
Text
and catching that as aNonFatal X
whereX
is the effect whose interpreter throwscould be an effect
CatchNonFatal
you use in the consumer that is interpreted asinterpretCNF :: Sem (CatchNonFatal X : NonFatal X InterpreterError : Error InterpreterError : r) a-> Sem r a
if the consumer needs a data type for the error, then it's obviously something non-specific and may be part of the API. I do sometimes use data type errors specific to an interpreter to decide on a consequence in a consumer, so this would enforce some discipline in that area
I'd just do an indirection:
Or is there some reason that wouldn't work?
Sorry, just noticed that's what Torsten suggested
@Love Waern (King of the Homeless) so what's your opinion on whether this is a sane use case for
Error
?If the exception is inherently part of the effect, so that you must have the user or want to enforce the user to deal with the exception, then yes, this is a good way to go about it.
An alternative way is to instead do the conversion at the interpreter level.
Take the example of, say, a REST api that you can do for something stateful that might fail:
That way, users can use
get
of theState RestfulState
effect, and have it raise an exception, without theError RestfulException
effect being in scope.You want the ability to deal with the exception in application code? No problem. Just have an
Member (Error RestfulException) r
at the place where you want to deal with the exception, and then usecatch
. Notably, you can do this without needingMember (Embed IO) r
like the interpreter does.There is a problem however: you can't, out-of-the-box, have more granular exceptions. Say you want to deal with a
RestfulException
generated byput
andget
, but in a granular way: in terms of anError
effect that's smaller thanError RestfulException
, or maybe not even connected toRest
at all: say,Error PutException
. If so, then you need an interpreter later down the line to convertError PutException
toError RestfulException
. You might reach formapError
, but NOPE, it will actually break due to #225. I hadn't actually realized this until now. The example in #225 is contrived in comparison.But the following will fix that:
Which I am now convinced should get added to Polysemy v2.0, probably together with a deprecation of mapError. For the moment, stick that into your code, and use it to convert between your
Error
s.This way, you can get:
1. The choice to be ignorant to the fact that an action of an effect may cause an exception generated by the interpreter for that effect
2. The ability to handle exceptions in a granular way when you do care about it
WASN'T DONE. HIT ENTER BY ACCIDENT
Now done.
Actually, now when I think about it, sometimes conversions only go one way. For example, it might make sense to convert
RestfulException
toMaybe PutException
, but notPutException
toRestfulException
. In that case, what you really want is anError
effect withoutthrow
to use in application code, à-lafused-effects
. That's something we'd need to think more about.my concern with
catch
is that the compiler won't notify me that I forgot to handle the interpreter's error in my program since usingcatch
doesn't remove the need for theError
interpreter after the throwing interpreter. Obviously this isn't exactly canonical forError
, but it's what happens to me a lot. I was wondering if I'm just doing things wrong.Hmm. Then what about combining the indirection approach with a pseudo-action that you can use in place of
catch
that does give you that property, like this:Then you can do this:
Hey, isn't this a trick some first-order extensible effects library used to get pseudo-error? Since with this, you don't need an higher-order effect for
catch
; you can use the pseudo-action instead.Yeah,
freer-simple
!There are probably some issues with this: I think it messes up local/global state semantics, since you're effectively taking a global "Error" effect and making it local. I'd need to look into it some more.
But all things considered, I quite like this. Shame you can't call it
catch
, though.... But maybe we can call it
handle
, if we decide to add it.I'm confused, you have the
Error
constraint ongetFooBar
now visible from the programsone
etc?With
catch'
, the error constraint is visible as long as there exists somegetFooBar
the possible exception of which hasn't been handled.Compare with
three
andusingTwo
; they don't have the constraint.my mind is going in circles now.
I was starting from this place when talking about
catch
:so here the fact that
E
is thrown bydoA
is not visible toprog
but it is also an error specific to
interpretA
and might not be thrown by a different interpreter, and moreover the different one might throw something elseso my earlier suggestion was to have an additional
AError Text
that is used as a something went wrong in A marker, which is then caught inprog
and converted fromE
at the interpretation pointand then my question was "how do I ensure that this error is handled in
prog
"Ok, I get it. Let me think a bit.
so this might be a completely unreasonable way to approach the whole subject but it's what transpired a lot for me
my earlier suggestion was to have an additional error effect that is used only for the purpose of connecting the two, as a constraint on
prog
let my try to implement this quick
I came up with this evil piece of magic:
But this is pretty damn ugly and bad
Wait, hold on... this ain't enough.
:slight_smile:
This is gonna get so damn ugly...
already interesting to see a HO effect specialized to
Sem r
interpreted :smiley:Going back to the FooAPI example, replacing
Error Text
withError SmallExc
.This is terrible, but it works, somehow.
FooAPIManager
is the "link", here. You can usegetFooBar
in a part of the program without knowing it throws exceptions, but eventually, in order to run that part, you must usemanageFooAPI
to convert it, at which point any exception makes itself known, and you must handle that exception by usingcatch'
.FooApiManager
has an additional type variable for the carrier, which is needed to actually interpretGetFooBar
with the parametrizedSem r0
seen insideManageFooAPI
, since we don't know anything about it other than it carriesFooAPIManager (Sem r) SmallExc
andError SmallExc
. By keeping that type variable polymorphic inside user code, you keep user code from doing naughty things with it.I also show here how you can do this in a way that exposes a smaller, granular exception to the user code rather than the bigger exception that the primitive may generate.
This is complicated, evil, and very, very ugly. I'll try to find some way to improve it.
Keep in mind I haven't checked if this compiles. It should work morally.
phew, I'll have to study that for a while
I haven't read this entire discussion, but it seems to me that I should just stick to returning Eithers
@Love Waern (King of the Homeless) can you tell me why this doesn't work? I'm trying to understand the semantics of throwing and catching.
the
throw
inintCNF
is caught byrunError
in the main function, notcatchNF
.Because
mapError
is broken.See #225
It's only just now I realized the extent of how horrible it is
Like I said above, use
errorToError
(which isn't inpolysemy
yet) insteadnope, same with
errorToError
:speechless:Oh, sorry. It's probably because
intNF
converts its argument as a whole, rather than individual throws. Same problem as withmapError
. That's also fixed by defining it in terms oferrorToError
.My suggestion for
mapErrorVia
in #225 has the same problem, so that's quite stupid of me in hindsight.ugh, obviously when I use
runError
, anX.catch
is inserted :smiley: now it works!even with
mapError
Cooked up an alternative to that manager garbage.
The key combinator of interest here is
exceptionally
, as well as the interpreterrunExceptional
.This has the benefit of shuffling more responsibility from the user to predefined functions. You don't need to understand how it works to use it!
Yeah, it uses a lot of internal stuff, but that's fine. We'll probably add
Exceptional
, or some generalization thereof.Also fun fact the only reason
Exceptional
instantiates its parameter is becauseweave
has a constraint it doesn't need. We'll fix that.nice, I'll have to try that out in the wild to assess the ergonomics.
For the record, this is what I bruteforced together to achieve the UX I was going for:
It's got a few holes :slight_smile: