Python Forum
Thread Rating:
  • 2 Vote(s) - 2.5 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Promise
#1
Earlier today, I had the opportunity (lol) to write a javascript fallback function for Promises for browsers which don't support Promises.  Which... is pretty much only IE (edge supports them just fine).  For reference: https://developer.mozilla.org/en-US/docs...ts/Promise

The basic idea, is that you create a promise and immediately return it, so the caller can attach callbacks which will be called when the Promise is fulfilled (...when the callee fullfills it's Promise that it'll eventually finish).  The use case for javascript is to make code look less disgusting when working with ajax calls.

For example, let's say you had a function get(url), which took, as a string argument, the url to request, and returned a Promise.  You could then write the following:
get("/some_content.json")
    .then(function(data) {
        alert(data);
    });

console.log("this will happen before the ajax callback");
It's basically just syntactic sugar, and is mostly useless now that javascript supports async/await, but that's beside the point.  The point is that it's actually pretty neat, and one of the cool parts is that you can chain data from one promise to the next, to create semi-functional programs.  And it got me to wondering how hard it would be to writing the same functionality in Python.

And it turns out... not very difficult at all.

class STATES:
    Pending = 0
    Resolved = 1
    Rejected = 2


class Promise:
    def __init__(self, worker):
        keys = [STATES.Resolved, STATES.Rejected]

        self.state = STATES.Pending
        self.callbacks = {key: [ ] for key in keys}
        self.content = {key: None for key in keys}

        def callback_generator(state):
            def callback(response=None):
                self.content[state] = response
                self.state = state
                self.__run(state)
            return callback

        worker(
            callback_generator(STATES.Resolved),
            callback_generator(STATES.Rejected)
        )

    def __run(self, key):
        # a callback is only ever called a single time
        while self.callbacks[key]:
            callback = self.callbacks[key].pop(0)
            # chain the output of one callback into the input of the next
            response = callback(self.content[key])
            if response is not None:
                self.content[key] = response

    def __chain_method(self, state, callback):
        self.callbacks[state].append(callback)

        # register the new promise's callbacks with the parent, so data/errors
        # will chain
        def worker(resolve, fail):
            self.callbacks[STATES.Resolved].append(resolve)
            self.callbacks[STATES.Rejected].append(fail)

        future = Promise(worker)
        future.content = self.content
        future.state = self.state
        if self.state == state:
            self.__run(state)

        return future

    def then(self, callback):
        return self.__chain_method(STATES.Resolved, callback)

    def catch(self, callback):
        return self.__chain_method(STATES.Rejected, callback)


if __name__ == "__main__":
    def callback(resolve, fail):
        print("before resolve")
        resolve(5)
        print("after resolve")
        # fail(Exception("boop"))

    future = Promise(callback)
    future.then(lambda x: x**2).then(print)
    #.catch(lambda err: print(f"Uh oh: {err}"))
Reply
#2
This all seems kinda crazy, so here's some more info.  Almost all code would be consumers of a promise, only a library would implement one.  The important part is that the code generating a promise should have a little bit of setup, but otherwise immediately return, and use the callbacks to signal when it's done doing it's work. 

Here's a sample use case, using a delayed timer with a callback, that gets called after that delay:
from promise import Promise
import threading
import time


def delayed(delay):
    def runner(resolved, failed):
        def thread():
            time.sleep(delay)
            resolved()
        threading.Thread(target=thread).start()

    return Promise(runner)


if __name__ == "__main__":
    def callback(*args):
        print("Inside callback")

    print("Before delayed callback")
    delayed(2.5).then(callback)
    print("After delayed callback")
    for _ in range(5):
        print("waiting...")
        time.sleep(1)
Output:
Before delayed callback After delayed callback waiting... waiting... waiting... Inside callback waiting... waiting...
Reply
#3
Wait wait wait.  It makes more sense for the Promise itself to handle threading, since almost anything that uses it would involve threading anyway.

import threading


class STATES:
    Pending = 0
    Resolved = 1
    Rejected = 2


class Promise:
    def __init__(self, worker):
        keys = [STATES.Resolved, STATES.Rejected]

        self.state = STATES.Pending
        self.callbacks = {key: [] for key in keys}
        self.content = {key: None for key in keys}

        def callback_generator(state):
            def callback(response=None):
                self.content[state] = response
                self.state = state
                self.__run(state)
            return callback

        self._lock = threading.Lock()
        self._thread = threading.Thread(target=worker, args=(
            callback_generator(STATES.Resolved),
            callback_generator(STATES.Rejected)))
        self._thread.start()

    def join(self):
        with self._lock:
            self._thread.join()

    def __run(self, key):
        # a callback is only ever called a single time
        with self._lock:
            while self.callbacks[key]:
                callback = self.callbacks[key].pop(0)
                # chain the output of one callback into the input of the next
                response = callback(self.content[key])
                if response is not None:
                    self.content[key] = response

    def __chain_method(self, state, callback):
        with self._lock:
            self.callbacks[state].append(callback)

            # register the new promise's callbacks with the parent, so data/errors
            # will chain
            def worker(resolve, fail):
                self.callbacks[STATES.Resolved].append(resolve)
                self.callbacks[STATES.Rejected].append(fail)

            future = Promise(worker)
            future.content = self.content
            future.state = self.state
            if self.state == state:
                self.__run(state)

            return future

    def then(self, callback):
        return self.__chain_method(STATES.Resolved, callback)

    def catch(self, callback):
        return self.__chain_method(STATES.Rejected, callback)


import time


def delayed(delay):
    def runner(resolved, failed):
        time.sleep(delay)
        resolved()

    return Promise(runner)


if __name__ == "__main__":
    def callback(*args):
        print("Inside callback")

    print("Before delayed callback")
    future = delayed(2.5).then(callback)
    print("After delayed callback")
    for _ in range(5):
        print("waiting...")
        time.sleep(1)
    future.join()
Output:
Before delayed callback After delayed callback waiting... waiting... waiting... Inside callback waiting... waiting...
Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020