2776. Convert Callback Based Function to Promise Based Function
Problem Description
The given problem outlines the requirement to transform a conventional callback-based function into a promise-based function in JavaScript. A callback-based function typically takes a callback as its first argument, with subsequent arguments being the data or parameters it operates on. The callback function usually has two parameters: the first is the error (if any), and the second is the result of the operation.
The promisify
function we need to write should accept a callback-based function fn
and return a new function that, instead of using callbacks, returns a promise. This promise should resolve if the original function's operation is successful, passing the result as the resolution value, or reject if an error occurs, passing the error as the rejection reason.
For instance, if we have a function sum
which adds two numbers but only if they're positive, it would call the callback with the sum if the numbers are positive or with an error if any of the numbers are negative. The objective is to create a promisified version of such a function, so instead of dealing with callbacks, we can work with the more modern promise structure that allows chaining and better error handling with try-catch
blocks in async
functions or with .then().catch()
chains.
Intuition
The intuition behind converting a callback-based function to a promise-based one lies in understanding the behavior of callbacks and promises. Callbacks are functions passed into other functions as arguments to be executed later, often upon the completion of an asynchronous operation. Promises are objects that represent the eventual completion or failure of an asynchronous operation and its resulting value.
When "promisifying" a function, we are essentially encapsulating the callback mechanism within the promise's resolve
(for success) and reject
(for failure) methods. By doing so, we create a function that no longer requires a callback function as a parameter and instead returns a promise that can be handled using .then()
for the resolved value or .catch()
for the rejected reason.
The solution code uses TypeScript, which allows specifying types for better code reliability and maintainability. The provided promisify
function returns an async
function that, when called, returns a Promise
. In the body of this function, the original function fn
is called with a special wrapped callback that invokes resolve
when the operation is successful or reject
when an error is passed to the callback. The rest of the arguments are directly passed to the original function with the help of the spread operator, ...args
.
This approach decouples the original function's logic from the handling of asynchronous execution, providing a more flexible and modern pattern for managing asynchronous operations in JavaScript.
Solution Approach
The solution approach for the promisify
function involves wrapping the callback-based function with a new function that returns a Promise
. We're using a closure here, a powerful feature in JavaScript where an inner function has access to the variables of its enclosing scope, to retain access to the original function fn
and its arguments.
Here are the steps the promisify
function performs:
-
Creating a New Function: The
promisify
function returns a newasync
function, which when called, will execute the original callback-based functionfn
. Asasync
functions always return a promise, it sets up a foundation for promisification. -
Returning a Promise: Within the returned function, we construct a new
Promise
using thenew Promise
constructor. The promise constructor takes an executor function with two parameters:resolve
andreject
. These are functions that change the state of the promise:resolve
will fulfill the promise, andreject
will reject the promise. -
Executing the Original Function: We then call the original function
fn
with a custom callback and the rest of the arguments. This custom callback adheres to the Node.js style of callbacks, where the first argument is an error, and the subsequent arguments represent successful response data. -
Handling the Callback Response:
- If the callback is called with an error as the second argument, we invoke the
reject
function, passing in the error. This changes the state of the promise to rejected and allows downstream error handling with.catch()
or atry-catch
block in anasync
function. - If the callback is called without an error, we pass the response data to the
resolve
method. This resolves the promise with the given value, which can be accessed through the.then()
method on the returned promise.
- If the callback is called with an error as the second argument, we invoke the
-
Spreading Arguments: The returned function uses the spread operator
...args
to pass all provided arguments to the original functionfn
, after the callback. This spread operator ensures that however many arguments are provided, they are passed on correctly.
The TypeScript types CallbackFn
and Promisified
describe the shape of the functions being dealt with:
CallbackFn
represents the original callback-based function. It accepts a callback function as its first argument followed by any number of number arguments.Promisified
represents the new function that returns a promise and takes the same number of number arguments asCallbackFn
(minus the callback).
The algorithm does not use complex data structures but relies on higher-order functions, closures, and the JavaScript promise mechanism to achieve the desired functionality. The algorithm is rather straightforward and does not involve any changes to the original function's logic but simply wraps it to provide a different interface.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Consider a simple callback-based function calculateArea
, which accepts a callback, length, and width. It calculates the area of a rectangle if both length and width are positive numbers. Otherwise, it returns an error through the callback:
1function calculateArea(callback, length, width) {
2 if (length <= 0 || width <= 0) {
3 callback(new Error('Length and width must be positive numbers'), null);
4 } else {
5 callback(null, length * width);
6 }
7}
To demonstrate the solution approach, we will promisify this calculateArea
function using the steps provided in the solution approach:
-
Creating a New Function: We would define
promisify
to return anasync
function that invokescalculateArea
. -
Returning a Promise: Within this new function, we encapsulate the logic of
calculateArea
by returning a newPromise
. -
Executing the Original Function: We call
calculateArea
with a custom callback that properly handles success or error using the promise'sresolve
andreject
functions. -
Handling the Callback Response: The custom callback uses the
error-first
callback pattern to determine how to settle the promise. -
Spreading Arguments: Instead of pre-defined parameters,
calculateArea
takes a list of arguments passed by spread syntax...args
.
Here is the promisified version of calculateArea
:
1const promisify = (fn) => {
2 return async (...args) => {
3 return new Promise((resolve, reject) => {
4 fn((error, result) => {
5 if (error) {
6 reject(error);
7 } else {
8 resolve(result);
9 }
10 }, ...args);
11 });
12 };
13};
14
15const promisifiedCalculateArea = promisify(calculateArea);
16
17// Now we can use the `promisifiedCalculateArea` function with promises:
18promisifiedCalculateArea(5, 3)
19 .then(area => console.log(`Area: ${area}`))
20 .catch(error => console.error(`Error: ${error.message}`));
21
22// If we try calling it with non-positive numbers, it will handle the error:
23promisifiedCalculateArea(-5, 3)
24 .then(area => console.log(`Area: ${area}`))
25 .catch(error => console.error(`Error: ${error.message}`));
In this example, calling promisifiedCalculateArea(5, 3)
will output "Area: 15" because the dimensions are positive. However, promisifiedCalculateArea(-5, 3)
will output "Error: Length and width must be positive numbers" because one of the dimensions is not valid.
This walk-through exemplifies how the promisify
function can take an existing callback-based function and adapt it to a promise-based pattern without altering the original business logic.
Solution Implementation
1from typing import Callable, Any
2import asyncio
3
4# Define a type for the callback function.
5CallbackFunction = Callable[..., Any]
6
7# Define a type for the resulting promisified function, which returns an awaitable yielding a number.
8PromisifiedFunction = Callable[..., Any]
9
10def promisify(function_with_callback: CallbackFunction) -> PromisifiedFunction:
11 """
12 Converts a callback-based function into a promise-based one.
13
14 Parameters:
15 function_with_callback (CallbackFunction): The original function that uses a callback.
16
17 Returns:
18 PromisifiedFunction: A function that returns an awaitable resolving to the same value.
19 """
20
21 # Return a new function that accepts an arbitrary number of arguments.
22 async def wrapper(*args: Any) -> Any:
23 # The new function returns an awaitable.
24 async def inner() -> Any:
25 loop = asyncio.get_running_loop()
26
27 # Invoke the original function with a callback function.
28 result, error = await loop.run_in_executor(None, lambda: function_with_callback(*args))
29 # If the callback is called with an error, raise an exception.
30 if error:
31 raise Exception(error)
32 # If the callback does not provide an error, return the result.
33 return result
34
35 return await inner()
36
37 return wrapper
38
39# Example Usage:
40# Define a function to be promisified.
41def example_function(callback, *args):
42 # Immediately call the callback function with a result value of 42.
43 callback(42, None)
44
45# Promisify the example function.
46async_function = promisify(example_function)
47
48# This is an example of how you might call the promisified function in an async context.
49async def main():
50 # Call the promisified function and print the result.
51 result = await async_function()
52 print(result) # Expected output: 42
53
54# To actually run the example, you would start the asyncio event loop like this:
55# asyncio.run(main())
56
1import java.util.concurrent.CompletableFuture;
2import java.util.function.BiConsumer;
3
4// Define a functional interface for the callback which will be used inside the method to be promisified.
5@FunctionalInterface
6interface CallbackFunction {
7 void apply(BiConsumer<Integer, String> callback, int... args);
8}
9
10// The promisified method will return a CompletableFuture that can contain the result of type Integer.
11@FunctionalInterface
12interface PromisifiedFunction {
13 CompletableFuture<Integer> apply(int... args);
14}
15
16public class PromisifyExample {
17
18 /**
19 * Converts a callback-based method into a promise-based (CompletableFuture) one.
20 *
21 * @param functionWithCallback The original method that accepts a callback.
22 * @return A method that returns a CompletableFuture resolving to the same value.
23 */
24 public static PromisifiedFunction promisify(CallbackFunction functionWithCallback) {
25 // Return a new method that accepts an arbitrary number of integer arguments.
26 return args -> {
27 // The new method returns a CompletableFuture.
28 CompletableFuture<Integer> future = new CompletableFuture<>();
29 // Invoke the original method with a BiConsumer as the callback function.
30 functionWithCallback.apply((data, error) -> {
31 // If the callback is called with an error, complete the future exceptionally.
32 if (error != null) {
33 future.completeExceptionally(new RuntimeException(error));
34 } else {
35 // If there is no error, successfully complete the future with the data.
36 future.complete(data);
37 }
38 }, args); // Pass the arguments to the original method.
39
40 return future;
41 };
42 }
43
44 // Example Usage:
45 public static void main(String[] args) {
46 // Promisify a method that uses a callback, which will immediately call the callback with a value of 42.
47 PromisifiedFunction asyncFunction = promisify((callback, arguments) -> callback.accept(42, null));
48 // Invoke the promisified method and log the result to the console.
49 asyncFunction.apply().thenAccept(System.out::println); // Expected output should be 42
50 }
51}
52
1#include <iostream>
2#include <functional>
3#include <future>
4#include <exception>
5
6// Define a type for the callback function which will be wrapped by Promisify.
7using CallbackFunction = std::function<void(std::function<void(int, std::string)>, int)>;
8
9// Define a type for the resulting 'promisified' function, which returns a future of an int.
10using PromisifiedFunction = std::function<std::future<int>(int)>;
11
12/**
13 * Converts a callback-based function into a future-based one.
14 *
15 * @param function_with_callback The original function that uses a callback.
16 * @returns A function that returns a future resolving to the same value.
17 */
18PromisifiedFunction Promisify(CallbackFunction function_with_callback) {
19 // Return a new function that accepts an integer.
20 return [function_with_callback](int arg) -> std::future<int> {
21 // The new function returns a future.
22 return std::async(std::launch::async, [function_with_callback, arg]() -> int {
23 // Create a promise to hold the result.
24 std::promise<int> promise;
25 std::future<int> result = promise.get_future();
26
27 // Invoke the original function with a callback function.
28 function_with_callback(
29 [&promise](int data, std::string error) {
30 // If the callback is called with an error, set the exception in the promise.
31 if (!error.empty()) {
32 promise.set_exception(std::make_exception_ptr(std::runtime_error(error)));
33 } else {
34 // If the callback does not provide an error, set the data in the promise.
35 promise.set_value(data);
36 }
37 },
38 arg
39 );
40
41 // Wait for the result to become available and return it.
42 return result.get();
43 });
44 };
45}
46
47// Example usage:
48int main() {
49 // Promisify a function that takes a callback, which will immediately call the callback with a value of 42.
50 auto async_function = Promisify([](auto callback, int) { callback(42, ""); });
51
52 // Invoke the promisified function and log the result to the console.
53 try {
54 std::future<int> future_result = async_function(0);
55 std::cout << "Result: " << future_result.get() << std::endl; // Expected output: 42
56 } catch (const std::exception& ex) {
57 std::cerr << "Error: " << ex.what() << std::endl;
58 }
59
60 return 0;
61}
62
1// Define a type for the callback function which will be wrapped by promisify.
2type CallbackFunction = (next: (data: number, error?: string) => void, ...args: number[]) => void;
3
4// Define a type for the resulting promisified function, which returns a promise of a number.
5type PromisifiedFunction = (...args: number[]) => Promise<number>;
6
7/**
8 * Converts a callback-based function into a promised-based one.
9 *
10 * @param functionWithCallback The original function that uses a callback.
11 * @returns A function that returns a promise resolving to the same value.
12 */
13function promisify(functionWithCallback: CallbackFunction): PromisifiedFunction {
14 // Return a new function that accepts an arbitrary number of numeric arguments.
15 return async function(...args: number[]): Promise<number> {
16 // The new function returns a promise.
17 return new Promise((resolve, reject) => {
18 // Invoke the original function with a callback function.
19 functionWithCallback((data, error) => {
20 // If the callback is called with an error, reject the promise.
21 if (error) {
22 reject(error);
23 } else {
24 // If the callback does not provide an error, resolve the promise.
25 resolve(data);
26 }
27 }, ...args); // Spread the arguments into the original function.
28 });
29 };
30}
31
32// Example Usage:
33// Promisify a function that takes a callback, which will immediately call the callback with a value of 42.
34// const asyncFunction = promisify((callback) => callback(42));
35// Invoke the promisified function and log the result to the console.
36// asyncFunction().then(console.log); // Expected output: 42
37
Time and Space Complexity
The time complexity of the promisify
function itself is O(1)
, meaning it runs in constant time. This is because the promisify
function merely constructs and returns a new function without performing any computation that depends on the size of the input.
The provided promisified
function will have the time complexity of the original CallbackFn
since it is essentially wrapping the existing callback function with promise handling logic. Hence, the time complexity of the promisified
function will be the same as the time complexity of the CallbackFn
it wraps. If the time complexity of CallbackFn
is O(f(n))
, where f(n)
is a function that describes how the execution time scales with the input size n
, then the time complexity of the promisified
function is also O(f(n))
.
The space complexity of the promisify
function is also O(1)
, or constant space complexity, as it doesn't allocate any additional space that depends on the input size. It just returns a new function object.
The space complexity of the returned promisified
function depends on the implementation of the original CallbackFn
. However, the use of a promise introduces additional space overhead. This space is for the closure that includes the original CallbackFn
, the resolve
and reject
functions, and any arguments passed to the promisified
function. In general, if the space complexity of CallbackFn
is O(g(n))
, then the space complexity of the created promisified
function would be O(g(n))
, not including the space used by the Promise internals, which is generally a constant overhead.
Which of the following shows the order of node visit in a Breadth-first Search?
Recommended Readings
LeetCode Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way we
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https algomonster s3 us east 2 amazonaws com recursion jpg You first
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time