Is there any way to resolve a promise synchronously? (or an alternative library that can)

折月煮酒 提交于 2021-01-01 04:56:26

问题


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
});


回答1:


You mention two different things:

  1. I want the interface to remain consistent

  2. [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.




回答2:


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 compute the result. Your mileage may vary.

Can you have stack safety without compromising 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 compiler 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.




回答3:


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.



来源:https://stackoverflow.com/questions/57305616/is-there-any-way-to-resolve-a-promise-synchronously-or-an-alternative-library

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!