Is there a way to represent IOEither and TaskEither as a single Monad that will also include the tryCatch
?
I currently will consume an API over HTTP, so it makes sense to use TaskEither, but anticipate that this code will migrate "closer to home" and it would make sense to make this an IOEither
at that point. So I want to write a consumer interface in tagless style
interface EngineRepository<M extends URIS2> {
calculateNumber: (i:SomeData) => Kind2<M, DomainError, number>
}
const getRepo = <M>(m:M extends URIS2):EngineRepository<M> => ({
calculateNumber: someCalculation(m)()
})
const calculateNumber = <M>(m:M extends URIS2) => flow(/* M.chain, M.map, etc. works great! */)
So far so good! However, while there is a tryCatch
for Option, Either, TaskEither, IOEither, etc., it's not part of any interface best I can tell. So I am trying to create my own:
interface Tryable<M extends URIS2> extends Monad2<M> {
tryCatch: <E,A>(f:Lazy<A>, onError: (reason:unknown) => E) => Kind2<M, E, A>
}
const calculateNumber = <M>(m:M extends URIS2) =>
flow(/* M.tryCatch works great now! */)
The problem here is that IOError
is sync, so f:Lazy<A>
is fine, TaskEither
is async, so it would need to be f:Lazy<Promise<A>>
instead.
Is there a better way to approach this, or is this not possible? Do I need to always use TaskEither but then add a step that turns the IOEither into TaskEither and give up on tagless final?
I have a tentative solution that feels hacky:
It works, but I don't like that it couples the monads and thunk types for two reasons:
if we implemented
Tryable
forOption
, the thunk type would still beIO
. Same forEither
and others. And that is where it starts feeling hacky, tightly couplingOption
andEither
withIO
!it's merely a fortunate coincidence thanks to duck typing that
Lazy<A>
is the same type asIO<A>
andLazy<Promise<A>>
is the same type asTask<A>
. If this were to change, this solution would not work.