plotly / Dash.jl

Dash for Julia - A Julia interface to the Dash ecosystem for creating analytic web applications in Julia. No JavaScript required.
MIT License
489 stars 40 forks source link

HTML templating style/API: varargs instead of do blocks? #7

Open mbauman opened 4 years ago

mbauman commented 4 years ago

I've really been enjoying Dashboards.jl — and am thrilled to see this becoming Dash.jl! I've been brainstorming a bit on how to make the HTML templating a bit simpler (and closer to both HTML & Julia syntaxes). One thought I've had is to de-emphasize (or maybe even deprecate/remove) the do block form and instead allow passing children as varargs to the components. Since we can pass keyword arguments before positional arguments, we can still have a decent structure.

As an example, DashboardsExamples/dash_tutorial/5_interactive_graphing_1.jl becomes:

html_div(
    # ...
    html_div(className="three columns",
        dcc_markdown(
            """**Hover Data**
            Mouse over values in the graph."""
        ),
        html_pre(id="hover-data", style=styles.pre)
    ),
    html_div(className="three columns",
        dcc_markdown(
            """**Click Data**
            Click on points in the graph."""
        ),
        html_pre(id="click-data", style=styles.pre)
    ),
    # ...
)

The biggest advantage over the do block form is that it's easier to catch mistakes. Forgetting a comma at the end of a line in a do block just simply ignores everything that precedes it! It's an error like this. I also think this matches Julia's style a bit closer while still allowing you to put ids and classes right up front (like in HTML).

waralex commented 4 years ago

Now for creating components you can use the functions with signatures: (; kwargs...), (childrens; kwargs...) and (children_maker::function; kwargs...). Add overload (args...;kwargs...) is not a problem. About the closeness to the HTML. For me personally, the do syntax is the closest to HTML:

<div class = "three columns">  #html_div(className = "three columns") do
      <div class = "three columns"> #html_div (className = "three columns") do
       .....
      </div> #end 
</div> #end

The main advantage for me is a clear separation of the element body from its parameters and getting rid of the hell of nested brackets. But this is my personal opinion.

About deprecate/remove it. If the Plotly team decides so, I will do it. But for me personally, this will mean extreme inconvenience in using such a package.

mbauman commented 4 years ago

Fair enough — this is a matter of syntax preference and so is quite subjective.

waralex commented 4 years ago

Overload is a wonderful mechanism and one of Julia's great advantages. So I don't really understand the suggestions to remove the signature in favor of "perhaps more convenient" if we can leave both and offer the user to decide on the situation. For example, for a large block, "do" may be convenient, and for small sub-blocks inside it, the option with brackets may be convenient

mbauman commented 4 years ago

It really only becomes more important for pedagogical reasons — I think it's helpful to have an opinionated standard style to promote readability and ease-of-use.

chriddyp commented 4 years ago

Re do / end: One issue that we've seen on the Python side of things is that it can be hard for users to figure out how to match closing parentheses and closing brackets with very nested layouts, especially if they aren't working in "proper" editors.

Do do & end have nice support in Julia editors for matching start and end?


[...] while still allowing you to put ids and classes right up front (like in HTML).

The ability to put short keyword arguments in the front of the declaration is very important. The use case is for when the declarations become very long and it becomes tricky to know which keyword arguments match up with which components.

On the Python side, we'll frequently have code that starts out looking like this where children is implicitly defined as the first argument:

html.Div([
    html.Div([
        # ...
    ]),

    # ...

    html.Div([
        html.Div([
            # ...
            # ...
            # This could be like 100 lines
            # ...
        ])
    ]),
])

and then, once more keywords are added to some of the components, morphed into this:

html.Div([
    html.Div([
        # ...
    ]),

    # ...

    html.Div(
        id='my-parent-div',
        style={'color': 'blue'},
        children=[
            html.Div(
                id='my-child-div',
                style={'color': 'red'},
                children=[
                    # ...
                    # ...
                    # This could be like 100 lines
                    # ...
                ]
            )
        ]
    )
])

Without the ability to put other keyword arguments up front, this code would look like this, where it becomes very difficult to see which keyword arguments apply to which components. Whenever children becomes more than e.g. 10 lines of code, we recommend that folks move their arguments up front.

html.Div([
    html.Div([
        # ...
    ]),

    # ...

    html.Div([
        html.Div([
            # ...
            # ...
            # This could be like 100 lines
            # ...
        ], id='my-child-div', style={'color': 'red'})

        # ...
        # There could be another set of components in here
        # that could be another 100 lines...
        # ...

    ], id='my-parent-div', style={'color': 'blue'}),
])
chriddyp commented 4 years ago

(In Python, we don't have the luxury of being able to have an arbitrary positional arguments after keyword arguments: def Div(id=None, style=None, *children) isn't valid syntax, so we couldn't use the syntax proposed in the initial comment https://github.com/plotly/Dash.jl/issues/7#issue-596804003. We had to stick with the convention of def Div(children=None, id=None, style=None) where children is implicitly the first argument so the keyword can either be omitted or it can be specified in a different position if explicitly provided.)

chriddyp commented 4 years ago

Another thing to consider is how easy it is for a user to assign children as a variable and pass it in.

For example, how easy is it for a user to unpack

html.Div([
    html.Div(id='1'),
    html.Div(id='2'),
    html.Div(id='3'),
], id='parent')

into

my_children = [
    html.Div(id='1'),
    html.Div(id='2'),
    html.Div(id='3'),
]

html.Div(my_children, id='parent')

In Python, if we had def Div(*children): (unpacked positional arguments), then it's actually conceptually a harder for users to pass in children as a variable.

Instead of html.Div(my_children) (as above), they would have to do:

html.Div(*my_children)

Unpacking arguments with * is an intermediate-advanced level Python concept and raises the barrier of entry.


Another note about our decision in Python to use explicit lists. In callbacks, users should be able to update their components's properties using the same data structure as they might use in app.layout.

So, if we have:

my_children = [
    html.Div(id='1'),
    html.Div(id='2'),
    html.Div(id='3'),
]

app.layout = html.Div(my_children, id='parent')

then in a callback, this could be updated with:

app.layout = html.Div(id='my-parent')

@app.callback(Output('my-parent', 'children'), [...])
def update_parent(...):
    return [
        html.Div(id='1'),
        html.Div(id='2'),
        html.Div(id='3'),
    ]

If we accepted positional arguments as children, then there could be some asymmetry with what is supplied directly as a component property vs how to update that component property with a callback.

By assymetry, I mean:

app.layout = html.Div(html.Div(id='1'), html.Div(id='2'), html.Div(id='3'), id='parent')

vs

app.layout = html.Div(id='my-parent')

@app.callback(Output('my-parent', 'children'), [...])
def update_parent(...):
    return [      # <-- Asymmetry here! Where did this list come from? We didn't need it in our layout
        html.Div(id='1'),
        html.Div(id='2'),
        html.Div(id='3'),
    ]

(The ... is just shorthand for stuff that I'm omitting because it doesn't matter, this isn't a feature of the language)


Another comment with do / end. One nice feature of jsx (and html and xml) is that the closing tags match the opening tags. Sometimes folks in the community wish we had that in Dash for Python as the end of some layouts can look like this:

                ])
            ])
        ])
    ])
])

whereas in JSX/HTML/XML, it looks like:

                </div>
            </div>
        </footer>
    </body>
</html>

of course frequently it just looks like this:

                </div>
            </div>
        </div>
    </div>
</div>

which isn't that much more helpful :smile_cat:

waralex commented 4 years ago

The ability to put short keyword arguments in the front of the declaration is very important. The use case is for when the declarations become very long and it becomes tricky to know which keyword arguments match up with which components.

This work now:

html_div(className="three columns", 
            [
            dcc_markdown(
                """**Hover Data**
                Mouse over values in the graph."""
            ),
            html_pre(id="hover-data", style=styles.pre)
            ]
        )

In Julia, an argument can't be both positional and named. In terms of the function signature, this looks like (positional arguments; named arguments). When calling a function, you can arbitrarily place named arguments between positional arguments:

julia> function a(args...; kargs...)
                 println(args)
                 println(kargs)
       end
a (generic function with 1 method)

julia> a(1, a=2, 3, b=5, 4, c=6)
(1, 3, 4)
Base.Iterators.Pairs(:a => 2,:b => 5,:c => 6)