Reactive Functions
Contents
Reactive Functions¶
A reactive function in Meerkat is a function that reruns when one of its inputs changes. This allows for creating functions that are responsive to the data they are given.
Definition - reactive function
A function that reruns when one of its inputs changes.
Reactivity is the essence behind building interactive user interfaces in Meerkat. Reactive code is “tracked” by Meerkat so that it can be run again based on if the respective inputs change.
These docs walk through what reactive functions are, how to use them, and how to write your own.
Getting Started with Reactive Functions¶
In any interactive application, certain functions will need to be rerun when states of variables in the application change. Meerkat provides a mechanism for this through the concept of reactivity, implemented using the @mk.reactive()
decorator.
In these pages, we will discuss
the concept of reactivity in Meerkat
how to make code reactive
common pitfalls and how to avoid them
a few examples of how to use reactivity
turning reactivity off
What is Reactivity?¶
Definition - reactive function
A function that reruns when one of its inputs changes.
In Meerkat, reactivity refers to the ability to rerun a function invocation when its inputs change.
The bodies of reactive functions are exactly like regular functions. The difference is that reactive functions are tracked by Meerkat, which handles re-invoking them behind the scenes.
Creating Reactive Functions¶
Meerkat provides the @mk.reactive()
decorator to designate a function as reactive. If you haven’t used Python decorators before, they are simply wrappers around a function. When we use @mk.reactive()
, the decorator will return a new function that is reactive.
To use mk.reactive()
, use standard Python syntax for decorators. There are three ways to do this:
Decorate a function with
mk.reactive()
:@mk.reactive() def add_by_1(a): return a + 1
Call
mk.reactive()
on a function:def add_by_1(a): return a + 1 add_by_1 = mk.reactive(add_by_1)
Call
mk.reactive()
on a lambda function:add_by_1 = mk.reactive(lambda a: a + 1)
All three methods have the same effect of creating a reactive function called add_by_1
that can be used in subsequent lines of code.
What does wrapping a function with mk.reactive()
actually do?
At a high level, these functions will be rerun when their inputs change.
We’ll look at this in more detail in a bit. First, though, we’ll talk about Marked
objects, which are intertwined with reactive functions.
Marked Inputs and Reactivity¶
So far, we learned a simple definition of reactive functions in Meerkat. This definition was almost, but not quite, complete.
Rather than a reactive function being rerun whenever any of its inputs are updated, it is actually rerun whenever any of its marked inputs are updated. This is a subtle but important distinction.
What objects can be marked? By default, standard objects cannot be marked. However, they can be wrapped in a Store
. Store
objects are markable, which means we can mark them for use with reactive functions. Python primitives, third-party objects, and custom objects can be wrapped in Store
objects to make them markable. Other objects in Meerkat like DataFrame
and Column
are also markable. All of these objects provide a .mark()
and .unmark()
method to control whether they are marked or not.
This means that for a function to react to changes in its inputs, its inputs must be marked. Let’s look at a few examples.
Important
Inputs into reactive functions must be marked for the function to react to changes in them.
import meerkat as mk
@mk.reactive()
def add(a, b):
return a + b
# c1 is updated if either a or b changes
a = mk.Store(1)
b = mk.Store(2)
c1 = add(a, b)
# c2 is updated only if a changes as b is unmarked
a = mk.Store(1)
b = mk.Store(2).unmark()
c2 = add(a, b)
# c3 is updated only if a changes as b is not markable
a = mk.Store(1)
b = 2
c3 = add(a, b)
# c4 will never be updated because neither a nor b are markable
a = 1
b = 2
c4 = add(a, b)
Most of the time, we won’t need to worry about marking Store
objects, since they are marked by default.
However, other Meerkat objects like DataFrame
and Column
are unmarked by default. If we want reactive functions to react to changes in them, we must call .mark()
on them prior to passing them to a reactive function.
@mk.reactive()
def df_sum(df):
return df["a"].sum()
df = mk.DataFrame({"a": [1, 2, 3]})
# c1 is not updated if df changes because DataFrames are unmarked by default
c1 = df_sum(df)
# c2 is updated if df changes as df is now marked
df.mark()
c2 = df_sum(df)
Takeaways:
Any inputs can be passed to reactive functions, but only marked inputs will be used to determine if the function should be rerun.
Wrap a Python primitive or custom object in
Store
to make it markable.Use the
.mark()
and.unmark()
methods to control whether a markable object is marked or not.Store
objects are marked by default, whereas other Meerkat objects are unmarked by default.
Chaining Reactive Functions¶
One of the reasons reactive functions are so powerful is that they can be chained together. This allows us to decompose code into neatly abstracted functions and then compose them together to create complex workflows.
Let’s look at an example with two reactive functions, add
and multiply
.
@mk.reactive()
def add(a, b):
return a + b
@mk.reactive()
def multiply(a, b):
return a * b
x = mk.Store(1)
y = mk.Store(2)
u = add(x, y) # u is Store(3), type(u) is Store
v = add(y, 7) # v is Store(9), type(v) is Store
w = multiply(u, 2) # w is Store(6), type(w) is Store
x.set(4, triggers=True)
# u is now Store(6), type(u) is Store
# v is unchanged
# w is now Store(12), type(w) is Store
Let’s break down this example:
add
andmultiply
are both reactive functions.u
depends onx
andy
, so it will be updated if either of them are updated.v
depends only ony
, so it will only be updated ify
updates.w
depends onu
, so it will be updated ifu
is updated.
When we update x
, only u
and w
are updated, by running add
and multiply
again respectively. If instead we updated y
, all three outputs u
, v
and w
would be updated, with add
running twice and multiply
running once.
In practice, Meerkat provides in-built reactive functions for many common operations on Store
objects. So we could have written the example above as:
x = mk.Store(1)
y = mk.Store(2)
u = x + y # u is Store(3), type(u) is Store
v = y + 7 # v is Store(9), type(v) is Store
w = u * 2 # w is Store(6), type(w) is Store
In the above example, the +
and *
operators were automatically turned into reactive functions. Other examples include mk.len()
, mk.str()
, and mk.sort()
. You can find a full list of the reactive functions that Meerkat provides here.
Turning reactivity off¶
In some cases, we may not want certain code to be reactive. To make code non-reactive, we can use meerkat.unmarked
.
Important
Use unmarked
to turn off reactivity for a function or block of code.
Like reactive
, unmarked
can be used as a decorator for functions:
@unmarked()
def add(a, b):
# This will never return a store because the function is unmarked.
return a + b
x = mk.Store(1)
y = mk.Store(2)
c = add(x, y) # c is 3, type(c) is int
It can also be used as a context manager:
def add(a, b):
# This will never return a store because the function is unmarked (by default).
return a + b
x = mk.Store(1)
y = mk.Store(2)
with unmarked():
c = add(x, y) # c is 3, type(c) is int
If reactive
functions are run in an unmarked context, they will not be reactive:
@reactive()
def add(a, b):
return a + b
x = mk.Store(1)
y = mk.Store(2)
with unmarked():
c = add(x, y) # c is 3, type(c) is int