I'm trying to understand the diference between E.altW and E.orElse, but both seem to do the same. For example, I can construct a Semigroup that concatenates parsing functions using either of them:
import * as E from 'fp-ts/Either';
import * as A from 'fp-ts/Array';
import { pipe } from 'fp-ts/function';
import * as S from 'fp-ts/Semigroup';
const parseString = (u: unknown): E.Either<string, string> =>
typeof u === 'string' ? E.right(u) : E.left('not a string');
const parseNumber = (u: unknown): E.Either<string, number> =>
typeof u === 'number' ? E.right(u) : E.left('not a number');
const ParseSemigroup: S.Semigroup<
(u: unknown) => E.Either<string, string | number>
> = {
concat(a, b) {
return (input) =>
pipe(
a(input),
E.altW(() => b(input))
);
},
};
const ParseSemigroup2: S.Semigroup<
(u: unknown) => E.Either<string, number | string>
> = {
concat(a, b) {
return (input) =>
pipe(
a(input),
E.orElse(() => b(input))
);
},
};
const manyParsers = S.concatAll(ParseSemigroup)(parseNumber)([parseString]);
console.log(manyParsers('99'));
const manyParsers2 = S.concatAll(ParseSemigroup2)(parseNumber)([parseString]);
console.log(manyParsers('99'));
And the result is exactly the same, as you can see. When should I use one over the other? or what are the specific usecases of each?
First, one thing to note is that altW
is slightly different from alt
and likewise for onElseW
and onElse
. The W
suffix stands for ‘widen’ and it means you can do things like this:
const result: Either<never, string | number> = pipe(
E.right(''),
E.altW(() => E.right(0))
)
where the resulting Either
has a union type inside of it. You can read more about this on the fp-ts FAQ.
Here’s the types of alt
and onElse
and their W
counterparts:
declare const alt: <E , A >(that: () => Either<E , A>) => (fa: Either<E , A>) => Either<E , A >
declare const orElse: <E1, A, E2>(onLeft: (e: E1) => Either<E2, A>) => (ma: Either<E1, A>) => Either<E2, A >
declare const altW: <E2, B >(that: () => Either<E2, B>) => <E1, A>(fa: Either<E1, A>) => Either<E2, B | A>
declare const orElseW: <E1, E2, B>(onLeft: (e: E1) => Either<E2, B>) => < A>(ma: Either<E1, A>) => Either<E2, B | A>
Because TypeScript types can look pretty messy, here’s the same thing but with a Haskell-like syntax:
alt :: (() -> Either e a) -> Either e a -> Either e a
orElse :: (e1 -> Either e2 a) -> Either e1 a -> Either e2 a
altW :: (() -> Either e2 b) -> Either e1 a -> Either e2 (b | a)
orElseW :: (e1 -> Either e2 b) -> Either e1 a -> Either e2 (b | a)
As you can hopefully see, the main difference between alt
and orElse
is that the function passed into orElse
takes in the error E1
. In your case where the function passed to onElse
ignores/does not use the error, alt
and onElse
are equivalent. In this case, alt
is the more appropriate function to use here.
If you want to return a new Either
with a different error and/or success type, then you should use altW
/orElseW
.
So, in summary:
orElse
/orElseW
alt
/altW
orElseW
/altW
orElse
/alt
You may have noticed that there’s another difference between alt
and orElse
: in orElse
, the either returned by the function can have a different error type E2
.
This is because the alt
function comes from the Alt
typeclass:
interface Alt<F> extends Functor<F> {
readonly alt: <A>(fa: HKT<F, A>, that: LazyArg<HKT<F, A>>) => HKT<F, A>
}
In the case of Either
, you can think of HKT<F, A>
like Either<E, A>
, so the E
has to be the same both in fa
and that
.