fp-ts: [RFC] Combining `Either`s with different error types
The problem
// example from https://github.com/natefaubion/purescript-checked-exceptions
import * as E from 'fp-ts/lib/Either'
import { pipe } from 'fp-ts/lib/pipeable'
type HttpError = 'HttpError'
declare function get(url: string): E.Either<HttpError, string>
type FsError = 'FsError'
declare function write(path: string, content: string): E.Either<FsError, void>
type Err = HttpError | FsError
const program = pipe(
get('http://purescript.org'), // error: Type '"HttpError"' is not assignable to type '"FsError"'
E.chain(content => write('~/purescript.html', content))
)
This is because E.chain
is inferred as
// relevant type here ---v
chain<"FsError", string, void>(f: (a: string) => E.Either<"FsError", void>): (ma: E.Either<"FsError", string>) => E.Either<"FsError", void>
Solutions?
- explicit cast
const writeE1: (path: string, content: string) => E.Either<Err, void> = write
const program1 = pipe(
get('http://purescript.org'),
E.chain(content => writeE1('~/purescript.html', content))
)
- explicit
mapLeft
const widen = E.mapLeft<Err, Err>(e => e)
const program2 = pipe(
get('http://purescript.org'),
E.chain(content =>
pipe(
write('~/purescript.html', content),
widen
)
)
)
- lifting with
flow
andmapLeft
import { flow } from 'fp-ts/lib/function'
const writeE2 = flow(
write,
widen
)
const program3 = pipe(
get('http://purescript.org'),
E.chain(content => writeE2('~/purescript.html', content))
)
- defining a
chain
overload that combines the error types
const flatMap: <E2, A, B>(
f: (a: A) => E.Either<E2, B>
) => <E1>(ma: E.Either<E1, A>) => E.Either<E1 | E2, B> = E.chain as any
const program4 = pipe(
get('http://purescript.org'),
flatMap(content => write('~/purescript.html', content))
)
Others?
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Reactions: 7
- Comments: 40 (7 by maintainers)
Commits related to this issue
- add W variants, closes #904 — committed to gcanti/fp-ts by gcanti 4 years ago
- add W variants, closes #904 — committed to gcanti/fp-ts by gcanti 4 years ago
The official way to do it is what people do in other languages (Haskell, PureScript, Scala) and what @mlegenhausen is doing in TypeScript: wrap (and / or transform) the sub-errors into a new sum type.
So given the following modules
the pattern is
Thank you all for your comments, looks like we all agree that an additional, more flexible version of
chain
(i.e. solution 4) would be idiomatic e pretty useful, so here’s a proposalEither
Reader
ReaderEither
etc…
@gcanti and everyone else who contributed, just wanted to say, great work bringing this in. It’s made my life so much easier!!!
PR: https://github.com/gcanti/fp-ts/pull/1198
Something missing?
Option 5. Define a utility function that works at type level for every instance of Chain, so that there is no additional boilerplate to define a Chain instance.
Another thought here…
I think the explicit
mapLeft
solution should be avoided, as this can lead to a number of unnecessary method calls that do nothing more than an identity function in the compiled script.The
flatMapOr
/flatMapAnd
solution looks pretty darn close to me. Definitely want something that works for all Chainable Types. Naming is the only thing I’m unsure about. Are there any analogs in the Haskell/Scala/Purescript world we can work with?Maybe I am a little bit uncool from a FP perspective 😉 but I use a wrapper union type for all possible errors I want to combine. Instead of widen you can then use a simple
mapLeft
.What I find beneficial is that the error represents the path where the error happend because in bigger application I wrap error wrapper types in other error wrapper types. Also are these type extendable. So you can add every metadata you want.
This, IMO, is a very important thing to figure out. I’ve been using fp-ts consistently for years in major products, and one of the most valuable tools is chaining methods that return Either/TaskEither. I’ve gotten into the (anti?)pattern of starting my chains with the initial param “Right” value but explicit sum type for all possible “Left” values in the chain… ie:
with 2.x out that doesn’t even work any more, as chaining through pipe fails when using sum types over methods that return union members (see #1028 )
I’m all for a method that widens the “Left” types on either - perhaps separate from chain? maybe
chainAndWiden
?App developper perspective here:
Sometimes we want the error to widen but would like to control it at some boundaries. Ability to understand from where an unwanted widening has happened is crucial.
From earlier testing,
flatMap
is the best one that fits that bill here. The widening conflict can be tracked using hover on the pipe stages, it’s pretty readable.If
UnionToIntersection
breaks I am probably going to hire a hitman, for the purpose of this discussion an instance of either that mangle errors can be found here: https://github.com/Matechs-Garage/matechs-effect/blob/master/packages/prelude/src/either.tsI have more overloading specific to effect but the base shows what’s needed to be extended specifically to either
I’m not sure we can trust this “hack”
Related: https://github.com/microsoft/TypeScript/issues/29594
@gcanti Looks good but what about
sequenceTW
/sequenceSW
/sequenceW
/traverseW
etc? They seem to be as useful as chain since it’s a very common task to combine multiple things in one. This may quickly get out of control.@raveclassic you may find a RWC using that pattern in this effect lib:
https://github.com/mikearnaldi/matechs-effect/blob/master/packages/effect/src/overload.ts#L152
@raveclassic @giogonzo the problem with
sequenceTW
/sequenceSW
is how to type them.@christianbradley yes
@raveclassic -
I think there’s a big difference here between
sequence
/traverse
andchain
/chainW
. Chain is a “flow” based operation that is commonly used nearly everywhere in functional code to replace try/throw/catch (or Promise.reject in the case ofTaskEither
). The move to a more purely functional methodology by removing classes for types in v2.x has made this even more tedious.Doesn’t seem like anyone’s banging the door down for
sequenceW
at the moment. If it becomes a highly requested item, what’s wrong with implementing it?In the end, a library like this needs to find the middle ground between perfectionism and usefulness. I’m all about it. Let’s get it in there.
@gcanti - Yes, Please! BTW - what is the meaning of the
W
suffix? “Widen”?Solution 4. seems to go nicely with monomorphic types, but it can get tricky with polymorphic types. Same example but with
Option
on the left:In this example
O.None | O.Some<"HttpError"> | O.Some<"FsError">
type is infered on the left, and AFAIU that is a tricky type to work with:@gcanti thanks for clarification. Seems to be the same pattern that I described only that I use a utility library for creating the new
Err
sum type.@steida
Actually we have a working solution described in this comment and starting with this one. I’m not sure if it should be added to the core (it’s up to @gcanti to decide) but it’s battletested in production in several our projects.
Yep, smth like this:
@raveclassic indeed! But I think that the decision should be made by the developer, there is no “default behaviour” that wont be sensible here. We can have 2 different methods to achive that, or use a type-level dictionary to choose between them.
Sometimes you want coproduct on the left (RemoteData, Either etc.) and sometimes product (Reader etc.). It depends on the position of the left value - covariant or contravariant.
I’ve ended up with a naive (and pretty ugly) solution. If you can “build” coproduct/product left for two values of the type, then you can actually combine more than two.
Hope that helps, we find such "combine"s very useful in our projects.
UPD: If such solution fits fp-ts design I could send a PR.