When should I use E.altW as opposed to E.orElse?

39 views Asked by At

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?

1

There are 1 answers

0
Lauren Yim On BEST ANSWER

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:

  • Do you need to use the error from the first either to return the second either?
    • Yes: Use orElse/orElseW
    • No: Use alt/altW
  • Does the second either have different types to the first either?
    • Yes: Use orElseW/altW
    • No: Use orElse/alt

Side note: Alternative

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.