Represent Task[Either] and IO[Either] as a single monad that includes tryCatch?

480 views Asked by At

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?

1

There are 1 answers

0
user1713450 On

I have a tentative solution that feels hacky:

interface Tryable<M extends URIS2, ThunkType extends 'Task'|'IO'> extends MonadIO2<M> {
  tryCatch: <E, A>(f:Kind<ThunkType, A>, onError: (e:unknown)=>E) => Kind2<M, E, A>
}

const te: Tryable<'TaskEither', 'Task'> = { ...TE.taskEither, tryCatch: TE.tryCatch}
const ioe: Tryable<'IOEither', 'IO'> = { ...IOE.ioEither, tryCatch: IOE.tryCatch}

pipe(te.tryCatch(()=>Promise.resolve(5),()=>'error'), te.map(num=>`${num}`)) // TaskEither<'error',string>

pipe(ioe.tryCatch(()=>5,()=>'error'), ioe.map(num => `${num}`)) // IOEither<'error',string>

It works, but I don't like that it couples the monads and thunk types for two reasons:

  1. if we implemented Tryable for Option, the thunk type would still be IO. Same for Either and others. And that is where it starts feeling hacky, tightly coupling Option and Either with IO!

  2. it's merely a fortunate coincidence thanks to duck typing that Lazy<A> is the same type as IO<A> and Lazy<Promise<A>> is the same type as Task<A>. If this were to change, this solution would not work.