briancavalier / creed

Sophisticated and functionally-minded async with advanced features: coroutines, promises, ES2015 iterables, fantasy-land
https://briancavalier.github.io/creed
MIT License
273 stars 20 forks source link

Folding #113

Open puffnfresh opened 7 years ago

puffnfresh commented 7 years ago

From the Bifunctor, it seems a Promise has two states:

  1. Error value
  2. Successful value

I would like to "run" a Promise and act on each state:

/* forall x. */ Promise.of(x).fold(a => false, b => true) == true
/* forall x. */ reject(x).fold(a => true, b => false) == true
never.fold(a => true, b => true) == undefined
briancavalier commented 7 years ago

Hey @puffnfresh. I'm having trouble wrapping my head around use cases of it for a few reasons. Do you have any in mind that you can share?

Where I'm getting hung up is it seems fold for any non-settled promise would need to return undefined (like your never example). E.g.:

delay(1000, 'delayed').fold(a => true, b => false) == undefined

That seems to imply that folks would often find themselves wanting to call isSettled before using .fold. Or perhaps it makes the type more like fold :: Promise e a ~> (e -> b) -> (a -> b) -> Maybe b. Do you have any thoughts on other ways it could work?

Creed promises aren't lazy, since one of the original design goals was to be ES-Promise compatible while also implementing Fantasy Land. So, fold wouldn't so much "run" the promise, as synchronously observe it's state at the instant of call (which may be why you quoted "run" in the first place).

Is that what you have in mind?

puffnfresh commented 7 years ago

Yes, I wasn't thinking right - it should always return undefined. I just want to use this for performing side-effects.

bergus commented 7 years ago

No, a promise has more than two states. It's not Either. It just allows to bimap over two of those states.

fold (like you described it) cannot return a b, as the e and a values will only be available asynchronously. At best, it could return a Promise Void b.

I still don't see your use case. Why not simply use .then(val => true, err => false)?

briancavalier commented 7 years ago

Thanks @puffnfresh. I think @bergus's question about .then is valid. People tend to use then for side-effects, but I'm interested to hear why you'd prefer an undefined-returning fold over ES then.

Thinking about it myself, I can imagine a few potentially interesting things about fold (I'm not sure all of these are actually good things, but they're at least interesting)

  1. The fact that it returns undefined is a reasonably clear signal of intent: you're going to consume the final error or value, and cause a side-effect.
  2. Because fold wouldn't return another Promise, the caller is assuming responsibility for any failure. An exception that escapes a function passed to fold could be made fatal (simply by creed's machinery not catching it and letting it hit the VM).
  3. The argument ordering is like bimap, further making then the outlier.
  4. Making the arguments required means you have to explicitly ignore the error by providing a function that squelches it.

Any thoughts on those?

puffnfresh commented 7 years ago

@briancavalier yeah, those are pretty much my thoughts.

Particularly the first one, if I want to do side-effects, I don't want to have a value.

briancavalier commented 7 years ago

It looks like there's some precedent for an operation named fold in fluture and folktale, which in creed's case would return a Promise.

So, maybe we actually want 2 operations here:

Map either side of the promise to a new fulfilled promise:

Promise e a ~> (e -> b) -> (a -> b) -> Promise e b

Consume either side of the promise and perform side effects:

Promise e a ~> (e -> b) -> (a -> b) -> undefined

Any thoughts on those two, and why we'd favor the name fold for one over the other? And what would we call the other?

briancavalier commented 7 years ago

FWIW, I'm still in favor of at least the "perform side effects and return undefined" function. I'm possibly in favor of the "map error or value to fulfilled" function, but seems harder to envision use cases for it.

briancavalier commented 7 years ago

Here's a proposal:

when :: Promise e a ~> (e -> f) -> (a -> b) -> undefined
fulfill(x).when(e => console.error(e), x => console.log(x)) === undefined // logs x
reject(e).when(e => console.error(e), x => console.log(x)) === undefined // logs e
never().when(e => console.error(e), x => console.log(x)) === undefined // doesn't log, ever

when():

  1. Calls the first function when the promise rejects, and the second when it fulfills,
  2. Discards the result of calling either function
  3. Does not catch exceptions thrown by either function (since they're called asynchronously, these will be fatal, i.e. they'll crash Node.
    • Note that if either function returns a rejected promise, creed's existing unhandled rejection machinery will make that rejection fatal as well (since it's discarded and no one can ever handle it)