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?)

Sorry I didn’t notice your reply earlier! Sure, you are welcome to decorate a function using pn.depends, and then ignore the fact that it’s been so decorated to use the function like you would have otherwise used it. At a technical level, that will work. However, I don’t think that’s a good practice because:

  1. Your business logic then gets tied up with Panel. Business logic should be able to be put into its own separate file or module and maintained for the long run, while user-interface libraries like Panel come and go. (In HoloViz we’ve had at least four of them come and go over the twenty-two-year lifetime of the project!) And what if you want to add a native iOS app alongside your Panel app that uses the same business logic but does not build on Panel? If you’ve decorated all your functions with pn.depends, you’ll now need to make sure Panel can be installed and is compatible with whatever iOS libraries you’re using for your GUI, causing you innumerable headaches for no reason.

  2. Even if you aren’t worried about the software maintenance and flexibility implications of tying your business logic functions to your GUI toolkit, e.g. if you are strict about delaying actually using the binding until later in your GUI code, it’s still not good practice to decorate your business logic functions with GUI markup, because it makes the code vastly more difficult for readers to reason about. People reading your business logic functions will never see anything about Panel or pn.bind if you defer the binding as is good practice, and thus never need to understand what it does and whether it matters to them. They can simply read a simple Python function that returns a value and appreciate what it does. But once you add a decorator, they will very likely see this function as “something to do with the GUI widget; not sure how that works but I better not mess with it”. If you take a given system and then link up all the parts of the system together so that understanding and reasoning about one part of the system requires understanding all of the system, the work required for a new person to understand it increases exponentially. Whereas if you present a bunch of independent functions (x does X, y does Y, z does Z) and then later you present a GUI that ties x and y to some buttons, it’s vastly easier for users to understand.

So yes, at a technical level the two approaches can be similar, but at a code-maintenance, flexibility, and human understanding level, they are miles apart!

1 Like