Imagine I am creating a function like the one below:
async function foo(axe: Axe): Promise<Sword> {
// ...
}
This function is designed to be utilized in this manner:
async function bar() {
// acquire an axe somehow ...
const sword = await foo(axe);
// utilize the obtained sword ...
}
Everything seems fine so far. The issue arises when I need to invoke a "callback-style async" function in order to implement foo, and I'm unable to modify its signature as it's part of a library/module:
/* The best way to obtain a sword from an axe!
* Returns null if axe is not sharp enough */
function qux(axe: Axe, callback: (sword: Sword) => void);
To address this, I decided to "promisify" Quux:
async function foo(axe: Axe): Promise<Sword> {
return new Promise<Sword>((resolve, reject) => {
qux(axe, (sword) => {
if (sword !== null) {
resolve(sword);
} else {
reject('Axe is not sharp enough ;(');
}
});
});
}
While this method works, I desired a more straightforward and readable solution. In some programming languages, it's possible to create a promise-like object (referred to as Assure
here), and then explicitly set its value elsewhere. It could look something like this:
async function foo(axe: Axe): Promise<Sword> {
const futureSword = new Assure<Sword>();
qux((sword) => {
if (sword !== null) {
futureSword.provide(sword);
} else {
futureSword.fail('Axe is not sharp enough ;(');
}
});
return futureSword.promise;
Is there a native way to achieve this in the language itself, or would I need to rely on a library/module like deferred?
Update (1): additional rationale
Why opt for the second approach over the first? Consider callback chaining.
What if I needed to execute multiple steps within foo, not just invoking qux? In synchronous code, it might resemble this:
function zim(sling: Sling): Rifle {
const bow = bop(sling);
const crossbow = wug(bow);
const rifle = kek(crossbow);
return rifle;
}
If these functions were asynchronous, promisifying would result in this structure:
async function zim(sling: Sling): Promise<Rifle> {
return new Promise<Rifle>((resolve, reject) => {
bop(sling, (bow) => {
wug(bow, (crossbow) => {
kek(crossbow, (rifle) => {
resolve(rifle);
});
});
});
);
}
By utilizing an Assure
, the implementation could appear as follows:
async function zim(sling: Sling): Promise<Rifle> {
const futureBow = new Assure<Bow>();
bop(sling, (bow) => futureBow.provide(bow));
const futureCrossbow = new Assure<Crossbow>();
wug(await futureBow, (crossbow) => futureCrossbow.provide(crossbow));
const futureRifle = new Assure<Rifle>();
kek(await futureCrossmbow, (rifle) => futureRifle.provide(rifle));
return futureRifle;
}
This method offers better manageability by eliminating the need to track nested scopes and worry about computation order, especially with functions that take multiple arguments.
Reflection
Although the version with nested calls may seem elegant due to fewer temporary variables declarations, the chained callbacks provide a cleaner structure.
Additionally, during the course of composing this inquiry, I considered another approach that aligns with JavaScript principles:
function zim(sling: Sling): Rifle {
const bow = await new Promise((resolve, reject) => { bop(sling, resolve); });
const crossbow = await new Promise((resolve, reject) => { wug(bow, resolve); });
const rifle = await new Promise((resolve, reject) => { kek(crossbow, resolve); });
return rifle;
}
This alternative resembles the usage of util.promisify
from Node.js. If only the callbacks adhered to the error-first convention... Nevertheless, it might be worthwhile to develop a rudimentary myPromisify
function to encapsulate the callback type handling.