Skip to content
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

Widget-binding API #1629

Closed
jbednar opened this issue Oct 13, 2020 · 7 comments
Closed

Widget-binding API #1629

jbednar opened this issue Oct 13, 2020 · 7 comments
Assignees
Labels
type: discussion Requiring community discussion type: feature A major new feature
Milestone

Comments

@jbednar
Copy link
Member

jbednar commented Oct 13, 2020

The API guide lays out four different APIs for using Panel, which one might think is already confusing enough, but even so I'd like to propose one more.

Parameterized param.depends API

Right now, one can write a Parameterized class using @depends("val") to indicate that a certain method should be called when parameter val changes:

import param

class C(param.Parameterized):
    val = param.Selector(dict(v1=1, v2=2))
    
    @param.depends("val")
    def output(self):
        return self.val

c=C(name="")

import panel as pn
pn.extension()
pn.Column(c.param, c.output)

image

Pros of this approach:

  • Concise
  • Separates Panel-specific (and thus JS, browser, Jupyter, etc.-specific) code from the domain code (C), allowing domain code to be defined in separate modules that don't import or otherwise require Panel

Cons:

  • Requires classes, which many Python programmers aren't familiar with
  • Requires understanding Parameters
  • Widget types are difficult to specify if you don't want the default widget for a given Parameter type

Widgets pn.depends API

A similar syntax is supported for bare functions, in this case depending on parameters stored on widgets:

import panel as pn
pn.extension()

valw = pn.widgets.Select(options=dict(v1=1, v2=2))

@pn.depends(valw)
def output(val): 
    return val

pn.Column(valw, output)

image

Pros of this approach:

  • Concise
  • Avoids having to understand classes or Parameters
  • Explicit about what type of widget you'll get
  • Conceptually related to the Parameterized API, making it simpler to switch between them

Cons:

  • Intertwines Panel and domain code, requiring widgets to be instantiated before code with dependencies can be defined
  • Confusing for users who want to understand output all by itself, but have to look up what depends does and understand how it might modify the function (e.g. wondering if it can still be called with an explicit argument for val, or has that argument been bound to valw permanently?)
  • Not clear how the same function could be used in multiple different Panel apps, each with their own choice of widgets, without redefining the function
  • If making a Panel for existing code where Panel and domain logic can't be intertwined in this way, requires writing a lot of wrapper functions just to express the binding from widgets to function arguments, then call the real code.

Widget-binding API

Can we get some of the same advantages of the Parameterized class approach for bare widgets? E.g. can we support a clean syntax for postponing binding to widget values until a function is actually used in Panel, like:

def output(val): 
    return val

import panel as pn, param
pn.extension()

valw = pn.widgets.Select(options=dict(v1=1, v2=2))
pn.Column(valw, pn.apply(output, val=valw))

This may already be possible by explicitly invoking pn.depends (rather than using it as a decorator), but if so I wasn't able to figure out the syntax.

Pros (if implemented):

  • Concise
  • Separates Panel-specific and domain code, allowing domain code to be defined in separate modules that don't import or otherwise require Panel
  • Avoids having to write wrapper functions unless more complex transformations are needed
  • Explicit about what type of widget you'll get, and allows that choice to be made independently for multiple panels that each use the same underlying code (leaving the widget details to the app, not the domain code).
@jbednar jbednar added the TRIAGE Default label for untriaged issues label Oct 13, 2020
@jbednar jbednar added type: discussion Requiring community discussion type: feature A major new feature and removed TRIAGE Default label for untriaged issues labels Oct 13, 2020
@hoxbro
Copy link
Member

hoxbro commented Oct 14, 2020

For the Widget-binding API part; if you need to use pn.depends without using it as a decorator it can be done like this:

def output(val): 
    return val

import panel as pn, param
pn.extension()

valw = pn.widgets.Select(options=dict(v1=1, v2=2))
pn.Column(valw, pn.depends(val=valw)(output))

Then maybe a simple wrapper like this could be made:

def pn_apply(f, *args, **kwargs):
    return pn.depends(*args, **kwargs)(f)

@philippjfr
Copy link
Member

Right, decorator syntax is clearly awkward so I wouldn't be opposed to a helper like @hoxbro proposed.

@jbednar
Copy link
Member Author

jbednar commented Oct 14, 2020

Ok, thanks. @hoxbro, I didn't try the double function call syntax; now it finally makes sense! But yes, that's awkward for end users, so I'd like to have a helper.

@philippjfr, any naming opinions? How I'd summarize what it does is create a function with one or more arguments bound to widgets in a way that when displayed in a panel, the function is invoked with the argument set to that value. There are lots of related concepts, and I think the naming choice should be driven by how readable and natural it is when used in code. E.g.:

pn.Column(valw, pn.bind(output, val=valw))
pn.Column(valw, pn.apply(output, val=valw))
pn.Column(valw, pn.funcall(output, val=valw))
pn.Column(valw, pn.partial(output, val=valw))
pn.Column(valw, pn.callback(output, val=valw))
pn.Column(valw, pn.reactive(output, val=valw))

Looking at that list and imagining writing the docs, pn.reactive is calling to me now, but it's totally up to you. Whatever name you pick I can make the PR and update the docs to match.

@mattpap
Copy link
Collaborator

mattpap commented Oct 14, 2020

pn.Column(valw, pn.bind(output, val=valw))

As naming conventions go, I think bind is the best from this list. callback and reactive are too overloaded already, apply and funcall imply (in my opinion) that output would be evaluated at this point and partial is a fairly different concept (it would make sense if output could be called with fewer number of arguments than specified in the signature, producing a new function).

@MarcSkovMadsen
Copy link
Collaborator

MarcSkovMadsen commented Oct 15, 2020

My few cents is that the name should signal that this is closely associated with depends.

What about pn.make_dependent, pn.mark_dependents or similar?

@jbednar
Copy link
Member Author

jbednar commented Oct 15, 2020

I think the name should be relatively short, as this is used in the "last step" of showing a panel, where a user will often be interacting directly. To show the relation to pn.depends, it could be pn.dependent, though that might make it difficult for people to remember which function to use where (between pn.depends and pn.dependent). Really if we want to show the relation to depends, we should also consider simply promoting direct usage of the decorator in our docs, without a helper function, which would also be ok by me (pn.depends(**kwargs)(fn)).

@philippjfr
Copy link
Member

+1 for pn.bind.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: discussion Requiring community discussion type: feature A major new feature
Projects
None yet
Development

No branches or pull requests

5 participants