Skip to main content

My journey to event-driven programming and cleaner code

· 5 min read
DALL·E prompt: A robot with big giant ears wearing headphones sitting in front of a floating data spreadsheet UI, TRON_ Legacy (2010)
Jake Mulford
How to leverage table listeners

Hi, I'm Jake, a software developer at Deephaven Data Labs on their developer relations team. Throughout my career, I've focused heavily on empowering other developers, ranging from being a TA in college to working on the developer experience of using REST APIs, and now working with Deephaven's query engine. This has exposed me to various approaches on how to develop software. Today I'm going to be talking about how recent experiences led me to the mindset of event-driven programming.

Event-driven programming is a very straightforward concept. Software is developed that executes after certain events happen. These events can be anything ranging from user actions, HTTP requests, or events from other threads. Deephaven has native support for event-driven programming with table listeners. Table listeners allow you to write functions that are executed when source tables change.

Simple listeners

Let's start with a simple example:

from deephaven.table_listener import listen
from deephaven import time_table

def listener_function(update, is_replay):
print(f"FUNCTION LISTENER: update={update} is_replay={is_replay}")

table = time_table("00:00:01").update("X=i").tail(5)
handle = listen(table, listener_function)

This listener simply makes a print statement with the information that has changed in the table. Nothing too complicated, right? The table is updated, and information is printed.

Now let's say we wanted to make different print statements based on whether the new number added was odd or even. We could update our listener_function with some logic to detect even and odd numbers, and that would look like this:

def listener_function(update, is_replay):
added_iterator = update.added.iterator()

while added_iterator.hasNext():
index = added_iterator.nextLong()
x = table.getColumnSource("X").get(index)

if (x % 2 == 0):
print("Even number found")
else:
print("Odd number found")

This certainly works, but that is only because our "events" are simply numbers being added. If this were a true event-driven system, we may have different functions we would want to execute based on the event, and that'd be very hard to wrap in a single function. You may be thinking "Okay, so we can instead write different functions based on the value in the table update, and our listener_function can contain logic to call the right function". That might look like:

def print_odd():
print("Odd number found")

def print_even():
print("Even number found")

def listener_function(update, is_replay):
added_iterator = update.added.iterator()

while added_iterator.hasNext():
index = added_iterator.nextLong()
x = table.getColumnSource("X").get(index)

if (x % 2 == 0):
print_even()
else:
print_odd()

This is now looking more like a traditional event-driven system. However, with Deephaven, you can go one step further. For those unfamiliar with Deephaven, one of its main features is handling real-time data with ease. One application of this is filtering data in real time.

Applying listeners to filtered tables

Instead of containing conditional logic within the listener_function, we can use Deephaven's filters to extract that logic and create two tables (one for odd numbers, and the other one for even numbers), and then register our print_odd and print_even functions as listener functions to these tables.

from deephaven.table_listener import listen
from deephaven import time_table

# Even though we aren't using the update/is_replay parameters, they are still required to be a listener
def print_odd(update, is_replay):
print("Odd number found")

def print_even(update, is_replay):
print("Even number found")

table = time_table("00:00:01").update("X=i").tail(5)
evens = table.where("X % 2 == 0")
odds = table.where("X % 2 == 1")

even_handle = listen(evens, print_even)
odd_handle = listen(odds, print_odd)

This approach has several advantages. We no longer need a general listener_function that contains conditional logic; that logic has been moved to Deephaven's filter methods. This allows us to use simpler listener functions for our tables.

Since we don't have a chain of if-else statements, we can add additional handlers without having to modify any existing code. For example, if we wanted a new handler for multiples of 3, all we would need to do is:

  • create a new table based on a filter on our existing table,
  • create a new listener function,
  • and register that listener function with the table.
def print_threes(update, is_replay):
print("Multiple of 3 found")

threes = table.where("X % 3 == 0")

threes_handler = listen(threes, print_threes)

Chaining conditionals is also easier. Let's say we had a special case and wanted an additional print statement for odd multiples of 5. With conditionals, we would need a nested if-else statement to do this. But with Deephaven, we can create a new table of odd multiples of 5 based on our existing table of odd numbers, and create a listener for that table.

def print_odd_fives(update, is_replay):
print("Odd multiple of 5 found")

odd_fives = odds.where("X % 5 == 0")

odd_fives_handler = listen(odd_fives, print_odd_fives)

Deephaven's listeners are extremely powerful, and exposure to them along with the Deephaven Query Language has been great for developing another way of looking at and solving problems.

We'd love to hear more about your own programming journeys and use cases -- join us on Slack!