question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

A case against Promise swallowing

See original GitHub issue

I’d like to make a few points about promise swallowing and how it is actually a harmful feature.

It breaks map

map should follow some laws

  1. f.map(g).map(f) is equivalent to f.map(compose(f,g))
  2. f.map(d => d) is equivalent to f

Now let’s try those laws out with promises

const filter = stream('');
// assume the results are of form {content: Result[]}
const getResults = f => fetch(`${baseurl}/search?q=${f}`);
const results1 = filter
.map(getResults)
.map(prop('content'))

const results2 = filter
  .map(compose(prop('content'), getResults);

isEqual(results1, results2) // false

Promise swallowing actually makes it unsafe to refactor map using the mathematical laws it’s supposed to follow.

It breaks ordering

Imagine the same example as above

const filter = stream('');
// assume the results are of form {content: Result[]}
const getResults = f => fetch(`${baseurl}/search?q=${f}`);

const results = filter
  .map(getResults)
  .map(prop('content'))

flyd.on(drawDOM, results);
document.querySelector('input.filter').addEventListener('input', filter);

Since the code handling promises in flyd is just

p.then(s);

No ordering is guaranteed. So if I quickly write some filter like: ‘res’, then 3 promises are generated and they can be resolved in any order, the stream will always contain the last resolved promise.

How do we fix these issues

It’s simple, remove promise swallowing in favour of a fromPromise method

let’s take the same example and rewrite it using the fromPromise helper

const filter = stream('');
const getResults = f => fetch(`${baseurl}/search?q=${f}`);

const results = filter
  .pipe(chain(compose(fromPromise, getResults)))
  .map(prop('content'))

flyd.on(drawDOM, results);
document.querySelector('input.filter').addEventListener('input', filter);

Now what has changed?

  1. We’re no longer breaking map
  2. Ordering is guaranteed since we’re always generating a new stream and chain stops listening to the old stream as soon as a new one is generated.

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Comments:10 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
paldepindcommented, Jan 30, 2018

@nordfjord

Promise swallowing actually makes it unsafe to refactor map using the mathematical laws it’s supposed to follow.

Good point. It’s actually worse than just breaking the laws. It also breaks the types of the Functor map. The type of map should be:

map<A, B>(f: (a: A) => B, s: Stream<A>): Stream<B>;

I.e. if we map with a function that returns B we should get a Stream<B> back. But, if the function returns Promise<B> we get Stream<B> instead of Stream<Promise<B>>.

Baking it into the library has the side effects you pointed out with, I’d argue, little benefit.

Thank you for weighing in as well @c-dante 👍 It’s good to get some input before we start removing features 😉

I’m convinced that we need to get rid of the inbuilt promise handling.

But, I don’t think fromPromise is a good enough replacement.

Consider the following example:

streamX
  .map(fetchNumberAsync)
  .pipe(scan((sum, n) => sum + n, 0));

If I refactor the above using the approach in https://github.com/paldepind/flyd/issues/167#issuecomment-361074117 I arrive at

streamX
  .pipe(chain(compose(fromPromise, fetchNumberAsync)))
  .pipe(scan((sum, n) => sum + n, 0));

However, these two pieces of code do not do the same thing. There is a subtle difference (please correct me if I’m wrong here @nordfjord). If two numbers are rapidly fetched after each other then the chain will unsubscribe from the first one and listen to the last one. This means that some numbers can get missed and that the count, in the end, migth turn out incorrect.

There are at least two different behaviors when handling promises:

  • React only to the last promise. This is what fromPromise and chain gives.
  • React to all promises. This is what the promise shallowing currently does.

The fast that we can support the first behavior is a really good thing. But if we remove promise shallowing we need a good migration story for people who currently rely on the last behavior as well.

I think we need a function like this (maybe not with that name though).

function flattenPromise<A>(stream: Stream<Promise<A>>): Stream<A> { ... }

The function should behave so that streamX.map(fnAsync) with the current promise shallowing is identical to flattenPromise(streamX.map(fnAsync)).

With this function the example in https://github.com/paldepind/flyd/issues/167#issuecomment-361074117 becomes:

streamX
  .map(doSomeAsyncStuff)
  .pipe(flattenPromise)
  .map(doMoreAsyncStuff)
  .pipe(flattenPromise)
  .map((result) => result.done ? true : doEvenMoreAsyncStuff(result.workLeft));
  .pipe(flattenPromise)

It’s still slightly more code but is probably an easier refactor to than the other one.

What do you think about that?

0reactions
paldepindcommented, Feb 8, 2018

@nordfjord I’m not sure I agree that having both is confusing. They do different things that are both “reasonable”? If properly documented I think people will be able to figure it out 😄 In fact I think they are quite different. One of them turns Promise<A> into Stream<A> and the other turns Stream<Promise<A>> into Stream<A>.

But I’m not opposed to having one of them in a module. Maybe we could even have both in a module? Another alternative for Flyd is to adopt ES modules. Then we could export all modules from one file and tree-shaking (as of Webpack 4) would take care of removing any unused modules.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Mindful Swallowing: The Promise of Motor Learning
Swallowing is something you shouldn't have to think about, says Ianessa Humbert unless you're a patient with a swallowing disorder.
Read more >
SWALLOWING EPISODE REPORT FORM (SERF) - CT.gov
Emergency Intervention First: Some observations, such as choking, require immediate emergency intervention to assist the person to survive a life-threatening ...
Read more >
swallowing_benefits.pdf - Passy Muir
We investigated the effect of a speaking valve (SV) on breathing-swallowing interactions and on the volume expelled through the upper airway after swallowing....
Read more >
Promise swallows mocha/chai assertion - Stack Overflow
Here's the immediate problem you are facing. The sequence of events is: Promise.all(emailPromises) resolves, so it calls the success ...
Read more >
Swallowing problems called dysphagia can kill you
Problems swallowing are a big killer, but the treatment can be horrible ... on the false promise of an improved life span or...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found