1. Introduction

In this article I want to discuss Promise in JavaScript and Deferred in Python Twisted (Deferred is also available in jQuery, and the idea is the same). They’re interesting, and a bit complicated. They played an important role in web programming before concurrency was widely used. Before that, let’s take a look at some basic concepts.

2. Starting with the request

Requests and responses

When we do network programming, we always use request and response . For example, if process A needs to pass some data to process B, we can say that process A sent a request to process B. Sometimes a request is sent and we don’t care about the rest; but more often, we care about the result of the processing of the data by the target process, and we want to process the result further. In this case, we can ask the target process to send a request to the source process and pass the result to the source process after it has received the sent data and processed it accordingly. We call this behavior a response.

Sessions

It is not enough to have a request and a response. Consider the following scenario:

image

Since the process of sending the request and the process of processing the response are located in two different methods, the context generated by the process of sending the request is not available when processing the response.

To solve this problem, we propose the concept of session. We believe that each request must correspond to a response, and a session id is used to uniquely identify a conversation, and the session id is passed between the request and the response. If some context is used in processing the response, it can be stored with the session id as the primary key, and the required context is retrieved by the session id during response processing. during response processing.

image

I wrote a simple example below, where the send method sends data to the remote process, the two arguments are the remote process handle and the data; when the process receives the data it calls back the on_receive_message method, the two arguments are the sender process handle and the data; request_handler is the request handler function, all request_handler` is the request handler function, which will be called for all requests.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function send_request(remote_process_handle, data, callback) {
    const session_id = generate_session_id();
    session_map[session_id] = callback;
    send(remote_process_handle, {type: 'request', session_id, data});
}

function on_receive_message(remote_process_handle, message) {
    if (message.type === 'request') {
        const response = request_handler(message.data);
        send(remote_process_handle, {
            type: 'response', session_id: message.session_id, data: response
        });
    } else if (message.type === 'response') {
        const callback = session_map[message.session_id];
        callback(message.data);
        delete session_map[message.session_id];
    }
}

The send_request function takes three parameters: the remote process handle, the request data and the callback function. Each call to send_request generates a session id, and stores the callback function with the session id as the primary key. When a response is received, the session id is used to find the callback function, and the callback function is called. This completes a session.

In this example, we use the js feature of using the callback function as the context. The real context, the ctx variable, is stored implicitly as the upper value of the callback function.

3. Introducing the problem

With a session, we can easily send requests and responses. For example, we can send a request and receive a response like this:

1
2
3
4
5
let ctx = dosth();
send_request(remote_process_handle, request, (response) => {
    calc(ctx, response);
    // ...
});

The target process can then receive the request and return the response:

1
2
3
4
function request_handler(request) {
    const response = deal_with(request);
    return response;
}

However, there are often some inconveniences in using callback functions in this way. What if you need to send a request to another process in the request handler and get the return value and then respond?

1
2
3
4
5
6
7
function request_handler(request) {
    let ctx = dosth();
    send_request(remote_process_handle, 'ask_for_response', (response) => {
        const res = calc(ctx, response);
        // Cannot return `res` as a response.
    });
}

We cannot return res to on_receive_message as the return value of request_handler, and we cannot pass the response of the request. To solve this problem, we can make some changes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function on_receive_message(remote_process_handle, message) {
    if (message.type === 'request') {
        request_handler(message.data, (response) => {
            send(remote_process_handle, {
                type: 'response', session_id: message.session_id, data: response
            });
        });
    } else if (message.type === 'response') {
        ...
    }
}

function request_handler(request, cb) {
    let ctx = dosth();
    send_request(remote_process_handle, 'ask_for_response', (response) => {
        const res = calc(ctx, response);
        cb(res);
    });
}

Instead of passing the response by the return value, a callback function is now passed with an additional parameter, and the response is passed by calling the callback function. This solves the problem.

However, this is still not very convenient, especially when multiple calls are involved, and the callback function needs to be passed through the parameters. Also, in many cases, a remote call may not always succeed, and we want to be able to handle exceptions when errors occur. So in addition to callbacks, it is common to pass in a function called an exception callback to specifically handle exceptions. If both callbacks and exception callbacks are passed in layers, this makes the code difficult to maintain and the methods containing the remote calls difficult to encapsulate into a common library.

4. Promise

To solve this paradox, js introduced the concept of Promise. A “promise” is a promise that some action will be performed in the future. The biggest change is that it moves the passing of callback functions from parameters to return values. A Promise object means an unfinished job that is promised to be completed in the future, and expects a callback function. When the work is completed at some point in the future, the callback function is called. This approach solves the problem described above perfectly.

Basic usage

Let’s look at the basic usage:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const promise = new Promise((cb, eb) => {
    setTimeout(() => {
        if (1 + 1 == 2)
            cb('ok');
        else
            eb('what the hell');
    }, 1000);
});

promise.then((res) => {
    console.log("result:", res);
}).catch((err) => {
    console.log("error:", err);
});

Constructs a Promise object using a function. This function takes two arguments: a callback and an exception callback. We say that a Promise object means an unfinished job, so the callback function is called when the job completes, and the exception callback function is called when the job fails. After constructing a Promise object, call Promise.prototype.then to set the callback function and call Promise.prototype.catch to set the exception callback function.

We can call the then method of a Promise object multiple times to set multiple callbacks. These callbacks form a chain of callbacks, and the return value of the previous function becomes the argument of the next function:

1
2
3
4
5
6
7
8
9
new Promise((cb) => cb(1)).then((res) => {
    console.log(res); // 1
    return res + 1;
}).then((res) => {
    console.log(res); // 2
    return res * 2;
}).then((res) => {
    console.log(res); // 4
});

These callback functions can also return Promise objects, forming nested Promise calls:

1
2
3
4
5
6
7
8
9
new Promise((cb) => cb(1)).then((res) => {
    console.log(res); // 1
    return res + 1;
}).then((res) => {
    console.log(res); // 2
    return new Promise((cb) => cb(res * 2));
}).then((res) => {
    console.log(res); // 4
});

The idea is the same, think of a Promise object as an unfinished task, and pass the result of the task when it completes by calling a callback function.

Using Promise

The above example uses Promise in this way:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function send_request(remote_process_handle, data) {
    const session_id = generate_session_id();
    /*construct a promise object, which means an unfinished job*/
    const promise = new Promise((cb) => session_map[session_id] = cb);
    send(remote_process_handle, {type: 'request', session_id, data});
    return promise;
}

function on_receive_message(remote_process_handle, message) {
    if (message.type === 'request') {
        const res = request_handler(message.data);
        Promise.resolve(res).then((response) => {
            send(remote_process_handle, {
                type: 'response', session_id: message.session_id, data: response
            });
        });
    } else if (message.type === 'response') {
        const callback = session_map[message.session_id];
        callback(message.data); // call `callback` function, fulfill the promise
        delete session_map[message.session_id];
    }
}

function request_handler(request) {
    let ctx = dosth();
    return send_request(remote_process_handle, 'ask_for_response').then((response) => {
        const res = calc(ctx, response);
        return res;
    });
}

where Promise.resolve automatically determines if an object is a Promise object, and if it is, it waits for it to be ready before calling it back, or if it is not, it calls it back directly.

As you can see, there is no need to pass the callback function in the argument, but instead the caller returns the Promise object and sets the callback function from it after receiving the Promise object. Even if there are multiple calls, you only need to return the Promise object one after another. This solves this problem perfectly.

Exception handling

Another powerful feature of Promise is exception handling. As mentioned above, in addition to calling Promise.prototype.then to set the callback function, you can also call Promise.prototype.catch to set the exception callback. Both callbacks and exception callbacks form a callback chain. When an exception occurs, the exception callback will be executed instead; and if the exception callback is fault tolerant, the callback will be executed instead.

image

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
new Promise((cb) => cb(1)).then((res) => {
    console.log(res); // 1
    if (res % 2 == 1)
        throw 'cannot be odd';
    return res + 1;
}).catch((err) => {
    console.log('error occurred:', err);
    // throw err;
    return 0;
}).then((res) => {
    console.log(res); // 0
}, (err) => {
    console.log('error occurred again:', err);
});

Promise.prototype.then supports both callbacks and exception callbacks, as shown above, the first argument is a callback and the second argument is an exception callback. If you uncomment throw err, it will print error occurred again: cannot be odd at the end.

5. Deferred

I was first introduced to Deferred using Python’s twisted library. Deferred is actually available in jQuery, and they are practically the same. The essence and core idea of Deferred is the same as Promise, but with a different presentation.

Basic usage

Again, let’s start with a simple example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from twisted.internet.defer import Deferred
from threading import Timer

def then(res):
    print("result:", res)

def catch(err):
    print("error:", err)

defer = Deferred()
defer.addCallback(then) \
     .addErrback(catch)

def on_timer():
    if 1 + 1 == 2:
        defer.callback('ok')
    else:
        defer.errback(Exception('what the hell'))

Timer(1, on_timer).start()

You can see that Deferred and Promise are quite similar. Deferred.addCallback is equivalent to Promise.prototype.then and Deferred.addErrback is equivalent to Promise.prototype.catch . The big difference is that instead of using a function constructor, Deferred calls Deferred.callback or Deferred.errback directly to tell it that its work has completed or failed.

Deferred’s callback chain and nested Deferred are the same as Promise, so here’s an example of the same:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from twisted.internet.defer import Deferred

def foo(res):
    print(res) # 1
    return res + 1

def bar(res):
    print(res) # 2
    defer = Deferred()
    defer.callback(res * 2)
    return defer

def baz(res):
    print(res) # 4

defer = Deferred()
defer.addCallback(foo) \
     .addCallback(bar) \
     .addCallback(baz)

defer.callback(1)

Using Deferred

As an example, here I have used Deferred to implement send_request and on_receive_message above.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def send_request(remote_process_handle, data):
    session_id = generate_session_id()
    defer = Deferred() # construct a deferred object, which means an unfinished job
    session_map[session_id] = defer
    send(remote_process_handle, {
        'type': 'request', 'session_id': session_id,
        'data': data})
    return defer

def on_receive_message(remote_process_handle, message):
    if message['type'] == 'request':
        res = request_handler(message['data'])
        def foo(response):
            send(remote_process_handle, {
                'type': 'response', 'session_id': message['session_id'],
                'data': response}))

        Deferred().addCallback(foo).callback(res)
    elif message['type'] == 'response':
        defer = session_map[message['session_id']]
        defer.callback(message['data']) # call `callback` function, finish the job
        del session_map[message['session_id']]

def request_handler(request):
    ctx = dosth()
    def foo(response):
        res = calc(ctx, response)
        return res

    return send_request(remote_process_handle, 'ask_for_response').addCallback(foo)

Deferred’s exception handling is basically the same as Promise, so I won’t go into it here. See the documentation for details.

6. Summary

I first came across Deferred when I was using Python twisted. At the time, I was using Python2, and there was no concurrency. It’s not as convenient as a concurrent process, but it solves the problem of repeated callbacks in server programming and improves maintainability of the code. Later, I came across Promise when I was using js, and found that they were similar. Of course, as technology evolves, these will slowly be replaced by concurrent processes. However, it is worth learning from their clever design ideas.