How to create a custom .andThen() chaining function for promises without adding to Promise.prototype?

Viewed 80

I have defined a custom Either type and have functions returning a Promise(Either(left,right)). To allow for chaining such functions I modified Promise's prototype like so:

Promise.prototype.andThenE = function(andThen) {
  return this.then(p => {
    return p.fold(
      e => Promise.reject(Either.left(e)),
      s => andThen(s),
    );
  }).catch(c => c);
};

This allowed me to write code like:

const response = await functionXReturningPromiseEither(input).andThenE(functionYReturningPromiseEither).andThenE(...);

I'm aware that extending native objects' prototypes is bad practice and I'm trying to yank this andThenE and put it directly on my Either and am unable to get it to work/chain correctly.

Here's my simple implementation of Either:

const Either = {};

Either.fromPromise = promise => {
  return promise.then(Either.right, Either.left);
};

Either.left = Either.failure = x => ({
  map: f => Either.left(x), //no-op: no mapping if "left" value
  fold: (l, r) => l(x), //apply "left" function to extract error value
  andThen: f => Either.left(x), //or flatMap or chain or join: Left(Left(x)) -> Left(x)
  dump: () => x, //for debugging
});

Either.right = Either.success = x => ({
  map: f => Either.right(f(x)), //map only if "right"
  fold: (l, r) => r(x), //apply "right" function to extract success value
  andThen: f => f(x), //or flatMap or chain or join: Right(Right(x)) -> Right(x)
  dump: () => x, //for debugging
});

If I do this it works flawlessly:

const f = async (arr) => Either.right(arr.map(x => x+1));

const res = await Either.fromPromise(Promise.resolve([1])).andThenE(f).andThenE(f);
console.log(res.dump());    //[3] - is of type Either.right([3])

However, I am unable to get andThenE to become a member of the Either in any meaningful way.

I tried doing this to no avail:

PromiseEither = promise => ({
  andThenE: f => {
    return PromiseEither(promise.then(p => {
      console.log("inside promise");
      return p.fold(
        e => Promise.reject(Either.left(e)),
        s => s.andThen(f),
      );
    }).catch(c => c));
  },
});

Either.fromPromise = promise => {
 PromiseEither(promise.then(Either.right, Either.left))
}

This doesn't succeed:

const res = await Either.fromPromise(Promise.resolve([1])).andThenE(f).andThenE(f);
console.log(res); //prints PromiseEither's structure and await is meaningless

I've tried many other variations to know avail. I thought it was straightforward but after hours of banging my head, I haven't made any progress. What may I be missing? Is this even possible?

Link to fiddle

1 Answers

First, I want to emphasise I would not do this. I don't think this is a great idea since promises already have an error handling model which is what Either is typically used for when I've used it.

I would instead start from scratch if you want - and use .then as an interop point between your type (a continuation with different error handling) and promises.

That said the standard way to do what you want is with subclassing (yuck) or Symbol.species (even yuckier). I'll show the first option and leave the second option as an exercise to the avid reader:

class PromiseEither extends Promise {
  andThenE(f) {
    return PromiseEither(this.then(p => {
      console.log("inside promise");
      return p.fold(
        e => Promise.reject(Either.left(e)),
        s => s.andThen(f),
      );
    }).catch(c => c)); // I would not do this .catch if I were you btw
  }
});

}

I would caution against promise subclassing - it's very tricky. Even getting a copy of Promise from a new realm and mutating the prototype (like an iframe or vm in Node) is better IMO.

Related