I have a method for validating a string, I want that method to return a Promise as the validations being ran may be asynchronous. The issue I am having however is one of performance, I want the promise to resolve in the same event loop when possible (eg: when there are no asynchronous validations to be done) but I want the interface to remain consistent (eg: to always return a Promise).
The simplified code example below illustrates what I'm trying to do, but it incurs the aforementioned performance penalties because even when the validation can be performed synchronously it still waits for the next event loop to process the result.
In my specific use case this performance penalty is too high.
Below is a simplified (minimal) example of what I'm doing
// Array containing validation methods
const validations = [
(value) => true, // Some validation would happen here
];
// Array containing asynchronous validation methods
const asyncValidations = []; // No async validations (but there could be)
const validate(value){
// Run synchronous validations
try {
validations.forEach(validation => validation(value));
catch(error){
// Synchronous validation failed
return Promise.reject();
}
if(asyncValidations){
return Promise.all(asyncValidations.map(validation => validation(value));
}
// Otherwise return a resolved promise (to provide a consistent interface)
return Promise.resolve(); // Synchronous validation passed
}
// Example call
validate('test').then(() => {
// Always asynchronously called
});
I have a method for validating a string, I want that method to return a Promise as the validations being ran may be asynchronous. The issue I am having however is one of performance, I want the promise to resolve in the same event loop when possible (eg: when there are no asynchronous validations to be done) but I want the interface to remain consistent (eg: to always return a Promise).
The simplified code example below illustrates what I'm trying to do, but it incurs the aforementioned performance penalties because even when the validation can be performed synchronously it still waits for the next event loop to process the result.
In my specific use case this performance penalty is too high.
Below is a simplified (minimal) example of what I'm doing
// Array containing validation methods
const validations = [
(value) => true, // Some validation would happen here
];
// Array containing asynchronous validation methods
const asyncValidations = []; // No async validations (but there could be)
const validate(value){
// Run synchronous validations
try {
validations.forEach(validation => validation(value));
catch(error){
// Synchronous validation failed
return Promise.reject();
}
if(asyncValidations){
return Promise.all(asyncValidations.map(validation => validation(value));
}
// Otherwise return a resolved promise (to provide a consistent interface)
return Promise.resolve(); // Synchronous validation passed
}
// Example call
validate('test').then(() => {
// Always asynchronously called
});
Share
Improve this question
edited Aug 1, 2019 at 9:19
RobC
25.1k21 gold badges84 silver badges85 bronze badges
asked Aug 1, 2019 at 9:15
user1878875user1878875
431 silver badge3 bronze badges
5
- Promises may fulfill/reject synchronously but reacting to those things is always asynchronous. This is quite by design in order to avoid zalgo and inconsistent APIs in which order is different. – Benjamin Gruenbaum Commented Aug 1, 2019 at 10:41
- @BenjaminGruenbaum Correct me if I'm wrong. I thought that the callbacks were called asynchronously only to prevent the call stack from blowing up. How would synchronous callbacks lead to inconsistency? – Aadit M Shah Commented Aug 1, 2019 at 11:14
- @AaditMShah oh hey - long time no speak :] Basically "zalgo" - that would mean that for users of someApi().then(callback) the callback can get called either synchronously or asynchronously and zero, one or multiple times. Promises guarantee that it's always called asynchronously (rather than sometimes) and that it's called at most once. – Benjamin Gruenbaum Commented Aug 1, 2019 at 11:35
-
@BenjaminGruenbaum Oh, all right. I understand. Hence, if you always call it asynchronously then you don't have to worry about when it might sometimes be called synchronously and other times be called asynchronously. Although, this wouldn't be problem if promises were lazy like the
Cont
monad. You build the putation lazily and when you finally call it, it can be either synchronous or asynchronous depending upon the putation. IMHO, this is much cleaner than maintaining states like the promises A+ specification requires. Anyway, how are you doing? What are you up to these days? – Aadit M Shah Commented Aug 1, 2019 at 12:18 - Ping me on Facebook or chat :) – Benjamin Gruenbaum Commented Aug 1, 2019 at 19:51
3 Answers
Reset to default 5You mention two different things:
I want the interface to remain consistent
[I want to] always return a Promise
If you want to avoid the asynchronous behaviour if it is not needed, you can do that and keep the API consistent. But what you cannot do is to "always return a Promise" as it is not possible to "resolve a promise synchronously".
Your code currently returns a Promise that is resolved when there is no need for an async validation:
// Otherwise return a resolved promise (to provide a consistent interface)
return Promise.resolve(); // Synchronous validation passed
You can replace that code with the following:
return {then: cb => cb()};
Note that this just returns an object literal that is "thenable" (i.e. it has a then
method) and will synchronously execute whatever callback you pass it to. However, it does not return a promise.
You could also extend this approach by implementing the optional onRejected
parameter of the then
method and/or the the catch
method.
The reason why promises resolve asynchronously is so that they don't blow up the stack. Consider the following stack safe code which uses promises.
console.time("promises");
let promise = Promise.resolve(0);
for (let i = 0; i < 1e7; i++) promise = promise.then(x => x + 1);
promise.then(x => {
console.log(x);
console.timeEnd("promises");
});
As you can see, it doesn't blow up the stack even though it's creating 10 million intermediate promise objects. However, because it's processing each callback on the next tick, it takes approximately 5 seconds, on my laptop, to pute the result. Your mileage may vary.
Can you have stack safety without promising on performance?
Yes, you can but not with promises. Promises can't be resolved synchronously, period. Hence, we need some other data structure. Following is an implementation of one such data structure.
// type Unit = IO ()
// data Future a where
// Future :: ((a -> Unit) -> Unit) -> Future a
// Future.pure :: a -> Future a
// Future.map :: (a -> b) -> Future a -> Future b
// Future.apply :: Future (a -> b) -> Future a -> Future b
// Future.bind :: Future a -> (a -> Future b) -> Future b
const Future = f => ({ constructor: Future, f });
Future.pure = x => ({ constructor: Future.pure, x });
Future.map = (f, x) => ({ constructor: Future.map, f, x });
Future.apply = (f, x) => ({ constructor: Future.apply, f, x });
Future.bind = (x, f) => ({ constructor: Future.bind, x, f });
// data Callback a where
// Callback :: (a -> Unit) -> Callback a
// Callback.map :: (a -> b) -> Callback b -> Callback a
// Callback.apply :: Future a -> Callback b -> Callback (a -> b)
// Callback.bind :: (a -> Future b) -> Callback b -> Callback a
const Callback = k => ({ constructor: Callback, k });
Callback.map = (f, k) => ({ constructor: Callback.map, f, k });
Callback.apply = (x, k) => ({ constructor: Callback.apply, x, k });
Callback.bind = (f, k) => ({ constructor: Callback.bind, f, k });
// data Application where
// InFuture :: Future a -> Callback a -> Application
// Apply :: Callback a -> a -> Application
const InFuture = (f, k) => ({ constructor: InFuture, f, k });
const Apply = (k, x) => ({ constructor: Apply, k, x });
// runApplication :: Application -> Unit
const runApplication = _application => {
let application = _application;
while (true) {
switch (application.constructor) {
case InFuture: {
const {f: future, k} = application;
switch (future.constructor) {
case Future: {
application = null;
const {f} = future;
let async = false, done = false;
f(x => {
if (done) return; else done = true;
if (async) runApplication(Apply(k, x));
else application = Apply(k, x);
});
async = true;
if (application) continue; else return;
}
case Future.pure: {
const {x} = future;
application = Apply(k, x);
continue;
}
case Future.map: {
const {f, x} = future;
application = InFuture(x, Callback.map(f, k));
continue;
}
case Future.apply: {
const {f, x} = future;
application = InFuture(f, Callback.apply(x, k));
continue;
}
case Future.bind: {
const {x, f} = future;
application = InFuture(x, Callback.bind(f, k));
continue;
}
}
}
case Apply: {
const {k: callback, x} = application;
switch (callback.constructor) {
case Callback: {
const {k} = callback;
return k(x);
}
case Callback.map: {
const {f, k} = callback;
application = Apply(k, f(x));
continue;
}
case Callback.apply: {
const {x, k} = callback, {x: f} = application;
application = InFuture(x, Callback.map(f, k));
continue;
}
case Callback.bind: {
const {f, k} = callback;
application = InFuture(f(x), k);
continue;
}
}
}
}
}
};
// inFuture :: Future a -> (a -> Unit) -> Unit
const inFuture = (f, k) => runApplication(InFuture(f, Callback(k)));
// Example:
console.time("futures");
let future = Future.pure(0);
for (let i = 0; i < 1e7; i++) future = Future.map(x => x + 1, future);
inFuture(future, x => {
console.log(x);
console.timeEnd("futures");
});
As you can see, the performance is a little better than using promises. It takes approximately 4 seconds on my laptop. Your mileage may vary. However, the bigger advantage is that each callback is called synchronously.
Explaining how this code works is out of the scope of this question. I tried to write the code as cleanly as I could. Reading it should provide some insight.
As for how I thought about writing such code, I started with the following program and then performed a bunch of piler optimizations by hand. The optimizations that I performed were defunctionalization and tail call optimization via trampolining.
const Future = inFuture => ({ inFuture });
Future.pure = x => Future(k => k(x));
Future.map = (f, x) => Future(k => x.inFuture(x => k(f(x))));
Future.apply = (f, x) => Future(k => f.inFuture(f => x.inFuture(x => k(f(x)))));
Future.bind = (x, f) => Future(k => x.inFuture(x => f(x).inFuture(k)));
Finally, I'd encourage you to check out the Fluture library. It does something similar, has utility functions to convert to and from promises, allows you to cancel futures, and supports both sequential and parallel futures.
Technically it would be possible to access a function the exact same way when it returns a promise or something else:
function test(returnPromise=false) {
return returnPromise ? new Promise(resolve=>resolve('Hello asynchronous World!')) : 'Hello synchronous World!'
}
async function main() {
const testResult1 = await test(false)
console.log(testResult1)
const testResult2 = await test(true)
console.log(testResult2)
}
main().catch(console.error)
You have to put all your code into any async function for that though. But then you can just use await, no matter if the function returns a promise or not.