Skip to content

Commit

Permalink
Updated reactive docs (#841)
Browse files Browse the repository at this point in the history
* Updated reactive docs

* Simplified flow of first example
  • Loading branch information
jbednar authored Sep 24, 2023
1 parent 3d4276c commit 6a20bf1
Showing 1 changed file with 151 additions and 23 deletions.
174 changes: 151 additions & 23 deletions doc/user_guide/Reactive_Expressions.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"source": [
"## Reactive Functions & Expressions\n",
"\n",
"In the [Dependencies and Watchers](Dependencies_and_Watchers.ipynb) guide we discovered how to express dependencies and write callbacks that are invoked when parameter values change. This imperative style of expressing dynamic behavior is powerful, but can create in complex code that is hard to read and maintain. Param 2.0 introduces a new set of primitives that make it possible to express complex dependencies in a declarative form, resulting in reactive functions and expressions that are re-evaluated automatically when required.\n",
"In the [Dependencies and Watchers](Dependencies_and_Watchers.ipynb) guide we discovered how to express dependencies and write callback functions that are invoked when parameter values change. This low-level, imperative style of expressing dynamic behavior is powerful, and works well for capturing complex behaviors in self-contained Parameterized classes with methods and Parameters. But even if you are not primarily designing a hierarchy of classes, it is still useful to be able to express dependencies between values and computations. Param 2.0 introduces a new set of declarative dynamic computation primitives that are useful even for single expressions, letting you write simple reactive functions and expressions that are re-evaluated automatically when required.\n",
"\n",
"The reactive programming model is one you might be familiar with from spreadsheets like Excel, where formulas can reference cells or ranges and dynamically (or more precisely, _reactively_) recompute when the inputs to a formula changes. In Param, Parameter objects correspond to a spreadsheet cell formula's inputs or references, and reactive expressions correspond to the formula itself. `param.bind` also allows the creation of a reactive function with arbitrary inputs.\n",
"\n",
Expand All @@ -22,39 +22,110 @@
"id": "c914e5f9-6c39-4dbf-b89c-d409842df3e0",
"metadata": {},
"source": [
"## Why?\n",
"## Getting started\n",
"\n",
"Before we dive in to discover how this works let's motivate it with an example. What reactive functions and expressions are great at is writing pipelines of operations in a natural form, i.e. without having to explicitly track and update state or the control flow.\n",
"Before we dive in to discover how this works behind the scenes, let's get started with a concrete example. What reactive expressions are great for is writing pipelines of operations in a natural form, i.e., without having to explicitly track and update state or the control flow. In most cases, you can simply write the same non-reactive Python code you always write, but then use arguments and option values that are reactive, so that the pipeline will re-run if the value changes.\n",
"\n",
"Let us take an example where we load some data using pandas and then transform it in various ways before displaying it. \n",
"\n",
"To begin with we will load some data using a reactive function:"
"For an example, let's load some data into a [Pandas](https://pandas.pydata.org) DataFrame and make it reactive:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b8bced20-02cf-4af1-83a8-47dd776a8779",
"id": "eaa1f75f",
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import param\n",
"import param.ipython\n",
"\n",
"url = param.reactive('https://datasets.holoviz.org/penguins/v1/penguins.csv')\n",
"from param import reactive as rx"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b8bced20-02cf-4af1-83a8-47dd776a8779",
"metadata": {},
"outputs": [],
"source": [
"URL = 'https://datasets.holoviz.org/penguins/v1/penguins.csv'\n",
"df = rx(pd.read_csv(URL))\n",
"df.head(2)"
]
},
{
"cell_type": "markdown",
"id": "71463561",
"metadata": {},
"source": [
"Here, this is just the same code you'd normally use to make a DataFrame, apart from using `rx()` to make the DataFrame into a reactive expression. As you can see, the reactive DataFrame works like any other DataFrame, using `.head()` and any other DataFrame methods as usual. But now, let's make the fixed number `2` above into a reactive expression, and see what happens:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ac49145c",
"metadata": {},
"outputs": [],
"source": [
"nrows = rx(2)\n",
"df.head(nrows)"
]
},
{
"cell_type": "markdown",
"id": "e90e96fc",
"metadata": {},
"source": [
"So far, nothing's changed. But what if we change the value of `nrows`?"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e9ead350",
"metadata": {},
"outputs": [],
"source": [
"nrows.rx.set_input(4);"
]
},
{
"cell_type": "markdown",
"id": "349ae1e6-d07f-4106-924f-21fbf6a4b0f7",
"metadata": {},
"source": [
"Whoa! As long as you are running a Jupyter notebook with a live Python process, you should have seen the dataframe \"head\" output _in_ _the_ _previous_ _cell_ update to the new value of `nrows`. That's because the reactive `df` expression in that cell captures the full pipeline of operations, automatically re-running `head` because the `nrows` has now changed. \n",
"\n",
"df = param.bind(pd.read_csv, url).rx()\n",
"We've done this without having to write any special callbacks or any new functions, instead using special Python objects that capture the operations you've invoked and replay them as needed when inputs change.\n",
"\n",
"df.head()"
"These updates should happen immediately (not only when the code cell finishes executing):"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "49b40993",
"metadata": {},
"outputs": [],
"source": [
"import time\n",
"\n",
"for i in range(4,9):\n",
" nrows.rx.set_input(i)\n",
" time.sleep(1)"
]
},
{
"cell_type": "markdown",
"id": "349ae1e6-d07f-4106-924f-21fbf6a4b0f7",
"id": "374de653",
"metadata": {},
"source": [
"Instead of having to write callbacks to operate on the DataFrame and update it we can now operate on the function's output as if it was the actual value."
"You should see the previous `df.head` output react to each time `nrows` is changed, updating to reflect the current state.\n",
"\n",
"We can get more complicated if we want, with a much more complex pipeline, but still matching the same code you'd write for a non-reactive Pandas DataFrame:"
]
},
{
Expand All @@ -66,9 +137,8 @@
"source": [
"import numpy as np\n",
"\n",
"nrows = param.reactive(10)\n",
"style = param.reactive('color: white; background-color: {color}')\n",
"color = param.reactive('darkblue')\n",
"style = rx('color: white; background-color: {color}')\n",
"color = rx('darkblue')\n",
"\n",
"def highlight_max(s, props=''):\n",
" if s.dtype.kind not in 'f':\n",
Expand All @@ -85,7 +155,7 @@
"id": "6f837bce-a2c2-4479-9adb-0a824fd29085",
"metadata": {},
"source": [
"And when we want to change the input(s) to the pipeline we can simply update them."
"Here we've made two additional reactive values (`style` and `color`), and written a Pandas pipeline that uses those values using precisely the same syntax you would with a regular Pandas expression. Since this is now a reactive Pandas expression, it will re-run whenever any of those changes. To see, try executing each of the following commands, one by one:"
]
},
{
Expand All @@ -95,19 +165,77 @@
"metadata": {},
"outputs": [],
"source": [
"url.rx.set_input('https://datasets.holoviz.org/gapminders/v1/gapminders.csv')\n",
"nrows.rx.set_input(12)\n",
"color.rx.set_input('red')\n",
"\n",
"styled_df"
"color.rx.set_input('red');"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "aa9f36b4",
"metadata": {},
"outputs": [],
"source": [
"nrows.rx.set_input(nrows.rx.resolve()+2);"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b3502bc8",
"metadata": {},
"outputs": [],
"source": [
"color.rx.set_input('darkblue');"
]
},
{
"cell_type": "markdown",
"id": "8082c9db",
"metadata": {},
"source": [
"In the code above, we made reactive strings, numbers, and DataFrame expressions. You can also make functions reactive, which lets you make the URL reactive as well:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "106363b4",
"metadata": {},
"outputs": [],
"source": [
"url = rx(URL)\n",
"df = rx(pd.read_csv)(url)\n",
"df.head(2)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "30d7e585",
"metadata": {},
"outputs": [],
"source": [
"url.rx.set_input('https://datasets.holoviz.org/gapminders/v1/gapminders.csv');"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "742bd80f",
"metadata": {},
"outputs": [],
"source": [
"url.rx.set_input(URL);"
]
},
{
"cell_type": "markdown",
"id": "60db0a0a-a2be-4927-91a3-7cc8b10ea313",
"metadata": {},
"source": [
"This approach makes it very easy to write code that is meant to re-evaluate dynamically in a natural form, making it extremely easy to write interactive tools and UIs."
"Here, `df` is no longer wrapping up a specific DataFrame and capturing operations on it, it's wrapping up the `read_csv` call that generates the DataFrame originally, but it all still works just the same. As you can see, reactive expressions let you write code just as you usually would, but then separately control all the reactive elements of it. \n",
"\n",
"Note that we have been using Jupyter Notebook cells as a way to change these reactive values, but if you imagine using a widgets from ipywidgets or [Panel](https://panel.holoviz.org) instead, you can see how easy it is to create a reactive computation or application with user-controllable options."
]
},
{
Expand All @@ -117,7 +245,7 @@
"source": [
"## Reactive Functions\n",
"\n",
"To get started, let's first write a simple, non-reactive function to add two arguments:"
"Ok, now that you've seen reactive expressions in action, let's dive into how this all works. We'll first write a simple, non-reactive function to add two arguments:"
]
},
{
Expand Down

0 comments on commit 6a20bf1

Please sign in to comment.