A case against Promise swallowing
See original GitHub issueI’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
f.map(g).map(f)
is equivalent tof.map(compose(f,g))
f.map(d => d)
is equivalent tof
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?
- We’re no longer breaking
map
- 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:
- Created 6 years ago
- Comments:10 (4 by maintainers)
Top 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 >
Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free
Top Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
@nordfjord
Good point. It’s actually worse than just breaking the laws. It also breaks the types of the Functor
map
. The type ofmap
should be:I.e. if we map with a function that returns
B
we should get aStream<B>
back. But, if the function returnsPromise<B>
we getStream<B>
instead ofStream<Promise<B>>
.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:
If I refactor the above using the approach in https://github.com/paldepind/flyd/issues/167#issuecomment-361074117 I arrive at
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:
fromPromise
andchain
gives.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).
The function should behave so that
streamX.map(fnAsync)
with the current promise shallowing is identical toflattenPromise(streamX.map(fnAsync))
.With this function the example in https://github.com/paldepind/flyd/issues/167#issuecomment-361074117 becomes:
It’s still slightly more code but is probably an easier refactor to than the other one.
What do you think about that?
@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>
intoStream<A>
and the other turnsStream<Promise<A>>
intoStream<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.