Callbacks vs async/await

A quick demonstration of callbacks vs async/await
Author

Wasim Lorgat

Published

February 13, 2023

import ipywidgets
import time
from threading import Thread

Let’s start with a very simple interface. An input text, a submit button, and an output text (disabled=True so that it’s not editable):

input_ = ipywidgets.Text("Hey ChatGPT, please summarise this text.")
button = ipywidgets.Button(description="Submit")
output = ipywidgets.Text(disabled=True)
display(input_, button, output)

This won’t do anything yet. We need to setup an on_click callback first. We’ll fake a request to OpenAI that simply sleeps for half a second then returns a fixed string. Then we’ll update the output text’s value with the result:

def request_open_ai(prompt):
    time.sleep(0.5)
    return "Here's a summary of your text."
def update_output(text):
    output.value = text
def on_click(button):
    text = request_open_ai(input_.value)
    update_output(text)
button.on_click(on_click)

If you click “Submit” now, it should populate the output after half a second.

If we do this synchronously and in the main thread, the entire UI will hang during the requestOpenAi call. So instead, we can separate that call into another thread. I think ipywidgets already does a version of this for us. But if it didn’t, here’s a very rough version of how we’d do it:

Thread(target=request_open_ai, args=(input,)).run()

But then how do we get the result and update the output with it? It’s often trickier to pass data across threads. Instead, we define our request function so that it accepts a callback that gets called with the result:

def request_open_ai(prompt, on_completion):
    time.sleep(0.5)
    on_completion("Here's a summary of your text.")
def on_click(button):
    Thread(target=request_open_ai, args=(input, update_output)).run()
input_ = ipywidgets.Text("Hey ChatGPT, please summarise this text.")
button = ipywidgets.Button(description="Submit")
output = ipywidgets.Text(disabled=True)
button.on_click(on_click)
display(input_, button, output)

This works okay, but starts to get very confusing as you add more and more nested callbacks. In fact, it can get so bad that it’s been nicknamed callback hell. Someone was so frustrated with it that they even created a website! This is where async/await becomes useful, since it looks a lot more like ordinary programming:

import asyncio
async def request_open_ai(prompt):
    time.sleep(0.5)
    return "Here's a summary of your text."
async def on_click(button):
    text = await request_open_ai(input_.value)
    update_output(text)

Note how these functions look a lot similar to the original synchronous ones instead of having the weird callbacks.

In most UI frameworks, we’d be able to pass in an async function like the new on_click. I’m not sure how to do that with ipywidgets, so we need to define a little wrapper that synchronously calls the async function, so we can set it as the button’s on_click handler (confusing, I know):

def on_click_sync(button):
    coroutine = on_click(button)
    asyncio.ensure_future(coroutine)
input_ = ipywidgets.Text("Hey ChatGPT, please summarise this text.")
button = ipywidgets.Button(description="Submit")
output = ipywidgets.Text(disabled=True)
button.on_click(on_click_sync)
display(input_, button, output)