10000 Revamped Reactive Expressions notebook by jbednar · Pull Request #858 · holoviz/param · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Revamped Reactive Expressions notebook #858

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Oct 6, 2023
Merged

Revamped Reactive Expressions notebook #858

merged 11 commits into from
Oct 6, 2023

Conversation

jbednar
Copy link
Member
@jbednar jbednar commented Sep 30, 2023

Reactive_Expressions.ipynb previously had three sections depending on these concepts:

  1. Getting started: rx() and .value
  2. Reactive Functions: Parameterized, Parameter, bind
  3. Reactive Expressions: .rx, Parameterized.param.X.rx(), .value, rx(), .rx.*

The first section depends only on two constructs: rx() and .rx.value, which seems nice and clean as an introduction. After that, though, immediately it dived into Parameters and into param.bind, both of which I consider optional for understanding and using reactive expressions. Instead, I've reorganized it into this outline to introduce concepts only as they are needed, to minimize how much people need to learn before they can start writing code:

  1. Getting started: rx() and .rx.value (complete basics, i.e. "what it is" and "that it works")
  2. Illustrating "how it works", so that it doesn't just seem magic
  3. Illustrating "that it doesn't always work", i.e., the finite list of limitations
  4. How to address the limitations in practice (the .rx namespace)
  5. Using Parameters with reactive expressions
  6. Bind and how/why to use it

Ideally there would also be a section 7 with a technical deep dive, but that's not something I propose to write now, and too much for this single nb.

Hopefully this is an improvement!

We don't necessarily need to address anything about widgets (ipy or Panel) in this release, but we should also keep in mind where those would be introduced, and maybe call them out more than I have so far.

We also may want to split the bind stuff into its own page alongside this one; it makes sense on its own and is a simpler story than reactive expressions in some ways, so it would probably be good to make it consumable without having to read about reactive expressions. But I've kept in here for now just because I don't have time to move it out, which I think someone should do but maybe isn't urgent.

@droumis droumis self-requested a review October 2, 2023 14:31
@maximlt
Copy link
Member
maximlt commented Oct 3, 2023

I fixed linting and added a note in the reactive guide about the API being experimental, since Jim added that note too in the release notes. And it makes sense to me marking it experimental for a little while.

@droumis
Copy link
Member
droumis commented Oct 3, 2023

looking it over now

@maximlt
Copy link
Member
maximlt commented Oct 3, 2023

Since #859 has been merged and fixed the ipython module being automatically imported when Param is imported, import param.ipython must be removed and the explanation about param.ipython must be adapted a little.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've gone ahead and committed a revision of several parts of this notebook in an effort to enhance clarity for non-param-experts like myself.

I could use a quick check if my mental model is fully accurate, but based on what I could understand, I've tried to expand on the comparison of the reactive expression approach to the existing 'push' and 'pull' models in Param. Further, I've described the .bind approach as a bridge between the 'push' and reactive 'rx' models... largely with the benefits of reactive programming and the debuggability of more explicit approaches. Does that seem correct?

I've also removed the import of param.ipython (and mention of this magic) in response to Maxime's comment above.

I'd appreciate a quick read-through from someone, but otherwise, I think I'm done.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't yet read your changes, a first reaction on introducing comparisons to the push/pull model is that I'm questioning whether this makes it easier for users to understand. I've had feedbacks that the Param user guides are too academic and I think that's one example of that 🙃 There's a bit of a terminology to understand for Param users, callbacks, events, depends, Parameters, and now there's even more with reactive expressions and functions. If we can not mention "push/pull" at all, I'd say it's a win.

(I'd bet the "pull" model (I mistakenly wrote "push" first hehe) - which is mostly numbergen - is barely used, if at all these days, one could argue numbergen should be a different project)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I share your concern and the fact that I found this guide previously too academic is exactly why I included the more intuitive language, descriptions, and use-case examples instead of just referring to the other APIs as "Dependencies and Watchers" and "Dynamic parameter values". In general, I think the introduction of a new API does require a comparison to existing modes of working with a package, even if only to convey how the new API serves a largely non-overlapping user base with the other approaches. I hope I achieved something reasonable in the revised notebook, but I'm open to further suggestions.

Copy link
Member
@philippjfr philippjfr Oct 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My immediate feeling is that we should minimize referring to dynamic parameter values, so I'd strongly prefer if we didn't compare and contrast the new API(s) against that. It's not a model that is used by any of our code these days and I would probably prefer if it stayed that way, so don't want to direct people towards that in any way.

Copy link
Member
@droumis droumis Oct 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok I'll just adjust to remove any mention of the pull approach in this notebook

< 8000 /summary> Copy link
Member
@maximlt maximlt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read the whole guide and still think it can be improved (happy to work on that!):

  • the guide relies too much on being executed in a notebook, I don't think you can understand what's going on without executing it to be honest. Either it needs to be updated or we need to allow users to execute the code (e.g. nbsite-pyodide directive, jupyterlite deployment, link to download the notebook).
  • if we want this API to feel very generic, I don't think we should start with a DataFrame example, for instance some simple string manipulation should do
  • explain that reactive expressions are by default not-reactive, and that's in line with the default behavior of param.depends and param.bind, they only become reactive when a framework supports them, and that Param happens to provide a built-in support for notebooks to automatically refresh the output of a reactive expression.
  • explain that setting a reactive expression with .rx.value = <> can only be done on the root
  • explain what happens when a reactive expressions has multiple "inputs", e.g. does it cache a re-use the value from a branch that is not updated?

"metadata": {},
"source": [
"Now, let's make a `Parameterized` class with some `Parameters`, here named `a` and `b`, that we'll want to add together reactively. We will also import `param.ipython` to install support for displaying \"live\" reactive components in an IPython or Jupyter notebook, re-rendering themselves when the inputs change."
"Without `rx()`, adding 1 to `i` would have immediately invoked integer addition in Python, assigning an integer 2 to `j`. However, because we made `i` a reactive expression, what happens is that `i` stores its input value on an internal attribute called `_obj`, while overloading `+` to not just calculate `i + 1`, but also return another reactive object that records the operation (`i + 1`). This stores the dependency so that whenever `i` changes, the reactive object knows that it needs to update itself by re-executing `i + 1`."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's something wrong, right?

while overloading + to not just calculate i + 1

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the question here?

< 8000 path d="M15 8a7.002 7.002 0 00-7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" vector-effect="non-scaling-stroke" />

"metadata": {},
"source": [
"Ok, now we can use `param.bind` to \"bind\" parameters `a` and `b` to create a reactive function:"
"## Limitations\n",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another limitation I believe we should add is that if i = rx(1), then isinstance(rex, int) is False.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you were proposinga fix for isinstance , so I didn't mention that. I agree it is focused on beginning able to execute it, and it would be great if it were live in the website because reactivity is hard to appreciate without seeing it react. I am not sure what you mean about the notebook being required, though; it's required only for the display to update dynamically, not the value.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you were proposinga fix for isinstance , so I didn't mention that

I suggested there was a way for insintance to operate on the underlying value that I think Philipp wasn't aware of; I don't think we've decided yet whether that's the right thing.

@maximlt
Copy link
Member
maximlt commented Oct 5, 2023

Answering to @jbednar in the main thread about:

I am not sure what you mean about the notebook being required, though; it's required only for the display to update dynamically, not the value.

I meant that the pipeline of a reactive expression isn't executed automatically when the value of the root is updated. It is only executed if you require the value on the resulting expression, or if a framework has registered a reactive display hook, like Param does automatically on import when in an IPython context.

image

@jbednar
Copy link
Member Author
jbednar commented Oct 5, 2023

The way I would say that is that whenever the value is accessed, it is always up to date, which seems like all you can ask of a reactive framework. It's lazy, yes, but reactive. So in e.g. a REST API, computed values will always be up to date when requested, without needing any special support in the event loop. All this should be explained, but I don't think it's misleading to show how it behaves in ipython since that's how it would behave for anything querying its value.

@maximlt
Copy link
Member
maximlt commented Oct 6, 2023

The way I would say that is that whenever the value is accessed, it is always up to date, which seems like all you can ask of a reactive framework. It's lazy, yes, but reactive.

I don't how other reactive frameworks operate, maybe that's how they all work, but trying to be in the position of a newcomer it seems to me that the combination of "lazy" + "reactive" is a bit strange. Why is it lazy by default, is it a design decision or is it the only way it can be?

We should indeed explain the way it works. Taking your REST example, if I build a reactive expression that returns the number of users in a group (group being the input), the result is going to depend on when the query is made. I.e. If I set group.rx.value and the result is computed only later when I access nusers.rx.value or immediately, the number returned can be different.

Generally as a developer I would like to know when the code is executed and how as that's useful for debugging purposes. Questions like the following ones will be asked sooner than later. When is a reactive expressi 62FB on lazy and when is it non-lazy (e.g. watching the reactive expression with .rx.watch(cb) makes it non-lazy)? Are all intermediate values cached? When a reactive expression has multiple inputs and only one of them is updated, is the whole graph re-executed or just the part that is affected by the updated input?

@maximlt
Copy link
Member
maximlt commented Oct 6, 2023

While it could be improved, the guide is already in a pretty good shape! Thanks all for contributing.

@maximlt maximlt merged commit 84ae7d6 into main Oct 6, 2023
@maximlt maximlt deleted the rxdocsvamp branch October 6, 2023 21:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants
0