Why is declarative approach favored over in efficiency imperative approach for interactivity?

I am specifically questioning the the example found here where the declarative solution is recommended over the imperative one, but I couldn’t find a clear reason why (it says “declarative approach enhances code maintainability and efficiency.” but doesn’t go further).

To me, both approaches in that example achieve the desired outcome in relatively the same manner, where the underlying dataframe turbines is filtered and assigned as the value to the Tabulator widget whenever any of the 3 parameters change in value.

Unless the declarative (.rx) approach evaluates the output lazily with some sort of planner, I don’t see why it would be more efficient?

P.S. doesn’t pn.rx actually incur additional overhead since it abstracts away the dependency tracking and management (chains operations implicitly)?

My understanding is the main recommendation is to avoid param.watch (side effects, or functions that don’t return anything, but modify an object’s param/property instead) because as the app grows, its hard to track down its dependencies.

rx on the flip side, tracks the dependencies, but can be slightly unintuitive starting out rx Marks the Spot: A Prescription for Reactive Python | by Andrew Huang | Medium

The sweet spot for me is using pn.bind. Developer Experience — Panel v1.6.3

Thanks for the clarification.

So, from an efficiency point of view, I don’t think establishing interactivity using side effects (via param.watch or pn.depends) are undesirable, right? I guess it’s just less preferred for maintainability?

Because in a real project for my work, I wrote a fairly complex (1.5k lines of code) yet performant app in Panel that achieves all its interactivity through side effects, as I almost never found a relevant use case for the return value from a reactive (bound) function (pn.bind is syntactic sugar around pn.depends anyways).

I have written a short summary here on the different ways to establishing interactivity in Panel, could you kindly give it a look and let me know if I got most of the concepts right or not?

I’m not 100% familiar with the Panel reactive internals so I’ve forwarded your question on our Discord channel Discord

1 Like

The main argument for a declarative or reactive approach is that it lets the user specify precisely what they want to happen at the user-visible level, and then the underlying watching mechanisms can be invoked automatically to make that happen behind the scenes. If you try to use the watching mechanism directly to build a complex system, you can be more explicit but you end up having to run a state machine in your own head rather than letting Param handle that. Humans are not great at running state machines (this invokes that, which invokes that, which notifies that, which responds to that, which sends a message to that, and eventually something happens), and so it’s increasingly cognitively difficult to reason about such a system as it gets more complex and changes over time, making the likelihood of major errors or unhandled states increase dramatically. Whereas declarative approaches like reactive expressions or bound functions are complete specifications of the desired outcome, each standing alone semantically even if they are linked in shared dependency chains, making them vastly easier to reason about.

I think some people have already rewired their brains into thinking like state machines or event loops, due to working with other GUI toolkits based on message passing or other lower level approaches, and so they like the more detailed control they get by thinking about each individual event. But I try never to let that happen to my own brain!

  1. Using pn.bind (also declarative approach): pn.bind(func, p1, p2, ...) is basically a wrapper (syntactic sugar) around @pn.depends and hence is heavier due to overhead and not preferred. Only use pn.bind if you really need to use the bound function’s return for some bussiness logic (dynamically computed values) in addition to reactive UI updates/rendering.

I don’t think that’s accurate. @pn.depends is appropriate within a Parameterized class, where it expresses a direct relationships between the Parameters defined in the class and the methods that depend on them. The decorator is perfect for establishing a clear link there, where the Parameter exists independently of some future widget that might represent it in a GUI. Outside of a Parameterized class, there aren’t any Parameters separate from widgets, and in this context pn.bind being built on pn.depends is simply an historical artifact, an implementation detail that should not be determining architecture choices. And the pn.bind approach has an absolutely crucial advantage over pn.depends outside of a class in that the binding can be deferred until the function is actually used in the context of a Panel app, thereby cleanly separating the underlying functionality of your function from its attachment to a widget in your GUI app. See Widget-binding API · Issue #1629 · holoviz/panel · GitHub for the reasoning involved here.

If pn.bind being a wrapper around pn.depends is truly slowing anything down (which seems doubtful), I’d support pulling it out and eliminating the extra function call. But definitely don’t use pn.depends in preference to pn.bind, because then you are tying up your non-GUI code with your GUI code in an inextricable mess, which makes it hard to make full use of your non-GUI code when you someday want to use it without Panel.

1 Like

Thanks a lot for the rich explanation, definitely helps a lot.

Quick followup questions:

  1. When using pn.depends outside of a Parameterized class (for GUI binding), and setting watch=False, wouldn’t that also defer the binding until the decorated-function is used for UI rendering (when laid out) in Panel, similar to what pn.bind does?
  2. I understand that with pn.bind you intend to keep your function Panel-agnostic (pure business logic), but even if you make it “tied” to your GUI with pn.depends, you could technically still reuse (call) it anywhere (for value return or side effects) as a pure Python function as long as you match the function’s signature (I don’t quite see an issue with that?)