dackerman / reflex-jsx

Use jsx-like syntax in Reflex
BSD 3-Clause "New" or "Revised" License
50 stars 7 forks source link

Questions about reflex-jsx limitations #2

Open meditans opened 7 years ago

meditans commented 7 years ago

Hi @dackerman, first of all thanks for this library! I was surprised on the ease of use, once integrated in my reflex project. Hi have a question on the limitation you talked about in the readme, though:

The only thing to remember is that you can't currently get values out of the nodes you insert, so for more complicated data flows, you might not be able to use a jsx block at all. In that case, it is probably easier to use functions anyway.

If I understand correctly, this is only a limitation of the library in its current form, right? What should be done to bind dynamic values out of the node we insert?

dackerman commented 7 years ago

Hi @meditans, thank you! That's correct - it should be possible to introduce some syntax to return arbitrary values from within a JSX block. Something like:

{myVal} <- [jsx| <div>myVal@{ return "hello"}</div>|]

Basically, you'd need to introduce a new record type declaration from within TH, and then client code can deconstruct it (with something like NamedFieldPuns shown above). I was messaged with this idea just a week ago, and they may be working on an implementation at some point. If you have other ideas, feel free to let me know or make a PR!

saurabhnanda commented 7 years ago

Trying to participate in a discussion I know absolutely nothing about, but just wondering how React's version of JSX solves this? Or do they somehow not need to solve this problem?

BartAdv commented 7 years ago

In React's JSX is simplier, because there's no monadic flow:

giveMeClickEventSomehow <- [jsx|<foo>...</foo>]

instead there is:

<foo onClick={this.someHandler()}>...</foo>
saurabhnanda commented 7 years ago

Just thinking aloud here, would this be easier to solve with Angular 2's approach? The HTML "extensions" to help them in tying click-handlers and bindings (dynamic values) to Javascript.

BartAdv commented 7 years ago

@saurabhnanda this approach is quite tempting, and it's being explored for example by http://try.websharper.com/example/todo-list (just check how they use F# type providers to have statically checked HTML!). I'm wondering, however, whether it's a good fit at all for library like reflex-dom, as it's much more expressive than the usual: "state -> DOM" rendering...

dackerman commented 7 years ago

Yes, Reflex and React are quite different in their approach to application development. I think of React as more of an "immediate mode rendering engine", which is to say it takes your properties and renders them whenever they change. You're expected to use the imperative nature of JS to handle events and tweak data as necessary - React doesn't have a lot to say about how you should do that (unless you want to use an addon like Redux which handles application state and data flow). Since any data could theoretically change at any time, React has a clever virtual-DOM-diffing mechanism to calculate the smallest DOM update and schedule it for rendering at the next animation frame.

Reflex, on the other hand, is a functional reactive programming library where streams of data/events are connected together to get interactivity. Reflex can know the parts of the application where change is even possible, and exactly what other streams can cause that change. This means the rendering engine can just render the DOM as it updates instead of needing to check the whole application each frame for changes (although I'm not 100% sure what the implementation looks like on the inside). Not only that, but you can end up with well-encapsulated components, where the inputs and outputs are well-defined and easy to test.

One consequence of this, though, is that more complicated apps tend to have lots of data flowing around, and look less like a static chunk of HTML (because event-handling is "local" instead of "out of band" like React). I think this is the reason that reflex-jsx has a problem that React itself doesn't seem to have, where it's useful to "return" values from within the jsx expression.

(Mostly just agreeing with @BartAdv, but wanted to provide a little more context in case it was useful)

FWIW, I am working on a solution to this - I haven't yet gotten something usable, but will ping this thread with the PR when I do, and I'm happy to get feedback on the design (as it will necessarily introduce new syntax)!

meditans commented 7 years ago

Hi @dackerman, thanks for the detailed info. Btw, if you want to share the work in progress, I may be able to help with some code, or give you early feedback!

saurabhnanda commented 7 years ago

much more expressive than the usual: "state -> DOM" rendering...

Reflex, on the other hand, is a functional reactive programming library where streams of data/events are connected together to get interactivity. Reflex can know the parts of the application where change is even possible, and exactly what other streams can cause that change. This means the rendering engine can just render the DOM as it updates instead of needing to check the whole application each frame for changes (although I'm not 100% sure what the implementation looks like on the inside). Not only that, but you can end up with well-encapsulated components, where the inputs and outputs are well-defined and easy to test.

Is this a good place to start a philosophical discussion about how exactly is Reflex (or FRP frameworks) better than React? Over at https://github.com/vacationlabs/haskell-webapps/tree/master/UI/ReflexFRP/mockLoginPage we're playing around with Reflex and I'm unable to get an intuition for this myself.

Is there a document which describes what is the problem in existing UI frameworks that Reflex is trying to solve?

dackerman commented 7 years ago

@saurabhnanda I highly recommend Ryan Trinkle's talks on reflex - part 1 and part 2. You can also discuss at https://www.reddit.com/r/reflexfrp/. Ryan is active on that subreddit so if it hasn't been asked before, you could bring up the topic and he's likely to weigh in.

meditans commented 7 years ago

Hi @dackerman I got around developing this feature: if you're interested we could talk a bit about the design of the library and then work towards a PR!

dackerman commented 7 years ago

Hi @meditans, @nmattia and I had been working on this to some extent, but I confess I don't have a lot of time so any help is appreciated! This issue is probably as good as any to discuss potential solutions (feel free to chime in, @nmattia, with your ideas or code as well).

Goals

Here are some goals I think we should strive for:

Things I'm not too concerned about:

Syntax

The syntax should be pretty unambiguous to parse, and low overhead to specify. My thoughts were something along the lines of {variableName@expression} to signify that it should return a record with the field variableName and value that expression evalulates to. I'm open to other ideas here.

Types

There are a couple ideas, I'll present both.

Idea 1

One idea is to allow the user to create a custom record for the return type of a jsx block.

Something like:

data UserWidget t = UserWidget
  { usernameInput :: TextInput t
  , submitClicks :: Event t ()
  }

ui :: MonadWidget t m => m (UserWidget t)
ui = do
  [jsx|
      <div>
        <label>Username: {usernameInput@textInput def}</label>
        {submitClicks@button "Submit"}
      </div>
      |] def 

Essentially, the JSX block itself would generate a function with a type signature like:

MonadWidget t m => UserWidget t -> m (UserWidget t)

The user would have to pass in a default value to the expression so it could build up the fields from within the function without needing to know how to create the type explicitly.

I mostly implemented this (as a prototype), but I ran into some issues with the typechecker needing a more explicit type signature on the function itself. Also, it's maybe not easy to make a default value for some types, and definitely requires some boilerplate.

Idea 2

Another idea is to generate a type in TH from within the block, and stick the fields on there. This would mean you'd have to do something like this (assuming NamedFieldPuns):

ui :: MonadWidget t m => m ()
ui = do
  {usernameInput, submitClicks} <- [jsx|
      <div>
        <label>Username: {usernameInput@textInput def}</label>
        {submitClicks@button "Submit"}
      </div>
      |]
  -- do something with the fields
  return ()

The downside is you wouldn't know the type, and so might be restricted in how you use the result (i.e. you have to destructure it or not specify a type). Maybe we could get around this with syntax like:

[jsx|UserWidget
    <div>
      <label>Username: {usernameInput@textInput def}</label>
      {submitClicks@button "Submit"}
    </div>
    |]

That would specify the type of the record to generate, resulting in something lik e data UserWidget = UserWidget {...}. The major roadblock I see to this approach is that I don't know how to infer the type of the fields from within TH. The field values are arbitrary expressions, so I don't if there is TH magic to figure out what the type of the whole thing is at generation time.

If anyone has ideas about how to do this, I am all ears!

Anyway, I appreciate people's interest - feel free to throw in your ideas or submit PRs for us to discuss.

nmattia commented 7 years ago

Here are some thoughts on the design.

Tuple based (HACK)

At some point I needed a solution quickly and didn't have too much time to spend on it. I simply returned a tuple of the value of the current call and the "rest":

<div></div>
<p>Hello</p>

will be turned into

(,) <$> (el "div" $ return()) <*> ((,) <$> (el "p" $ text "Hello") <*> ())

The branch is here.

I used (,) to carry the return type. However for nodes with n children we could return an n-tuple, which would make the matter a bit nicer. Also we can use lens to get values out, once we know where that value actually is. This highlights the biggest limitation: we can't really name the values. And because of that we depend on the structure of the returned object, which will change whenever the quasiquote changes. Also the return type is polluted with a lot of values that we don't need.

error:
    • Couldn't match expected type ‘((),
                                     (((),
                                       (((), ((), ())),
                                        ((),
                                         (((),
                                           (((),
                                             (a,
                                              ((),
                                               (((), (b, ((), ((), ())))),
                                                ((), (c, ((), (d, ())))))))),
                                            ())),
                                          ())))),
                                      ()))’
                  with actual type ‘(a, b, c, d)’

The error messages aren't actually that bad, the types are. I usually copy paste the type from the error message as the value-level tuple. Ew.

Map based

We replace all (named) values of type m a with a value of type m (Map String a). Then we append the maps together and return it to the user. The user can then lookup values using a String as the key. Obviously the main drawback here is that we lose some safety: the user gets a Maybe a from lookup even though we should be able to assure that a key/value pair is present. Also, I'm not sure how to deal with the fact that all values have different types.

I haven't tried implementing this one, so I won't say too much.

Easy to implement

Pattern match and NamedFieldPuns

I used to really like that one, however now I see a few flaws with the design. First, we need a constructor to pattern match on. That is you can't just call

{x, y} <- [jsx|<div>x@{return 2}y@{return 3}</div>

instead you'd need

C {x, y} <- [jsx|<div>x@{return 2}y@{return 3}</div>

There are several ways to fix it. The first one is to let the user provide either a datatype or even a default value, as you suggest in Idea 1. The second one is to let TH generate a datatype and corresponding constructor. How do we do that? Does the user pass in a String that will be the Constructor's name?

ACons {x, y} <- [jsx|(ACons)<div>x@{return 2}y@{return 3}</div>|]

Either way TH is "happening" outside the quote and either is allowed to modify the environment, like adding new functions into scope, or the user has to somehow leave clues to tell the quasiquote what to generate.

Symbol indexed lists

The [jsx|...|] returns a (heterogeneous) list of all the named values. However the values are tagged with the name they were given, at the type level:

ex1 :: (MonadWidget t m) => m (HList '[ '(Int, "x")])
ex1 = [jsx|<div>x@{return (1 :: Int)}|]

ex2 :: (MonadWidget t m) => m (HList '[ '(Int, "x"), '(String, "str")])
ex2 = [jsx|<div>x@{return (1 :: Int)}<p>str@{return "1234"}</p>|]

Using type application we can retrieve the values, indexing into the list with Symbols:

ex3 :: (MonadWidget t m) => m Int
ex3 = (unWrap @"x" . extract) <$> [jsx|<div>x@{return (1 :: Int)}<p>str@{return "1234"}</p>|]

ex4 :: (MonadWidget t m) => m (Dynamic t Bool)
ex4 = (unWrap @"check" . extract) <$> [jsx|<div>
                                              x@{return anInt}
                                              <p>
                                                check@{_checkbox_value <$> checkbox True def}
                                              </p>
                                            </div>|]

Here's a proof of concept.

It is actually very similar to the tuple idea.

ex5 = unWrap @"foo" <$> [jsx|<div>foo@{...}</div>|]

To be honest, I really like the last solution. It is type safe and uses non-TH, standard Haskell mechanisms. We could even provide Proxy based functions.

nmattia commented 7 years ago

Alright, I've made some more progress. There's now better syntax, and I fixed the parser (which I had broken). Also I was wrong yesterday, there are two extensions that you need to enable: TypeApplications and DataKinds.

Here's what the syntax looks like:

example :: (MonadWidget t m) => m Int
example = pick @"x" <$> [jsx|<div>x@{return 42}</div>|]

See tests on the branch for more examples. Also, I've moved a project to use this and I've faced no major problem.

dackerman commented 7 years ago

The above syntax is definitely more palatable, nice work! I had a few questions about this option:

Also, one comment about TH being "within" or "outside" the quotation. After thinking about it more, I think the most intuitive thing for a jsx block would be to just create local variables in the current scope for each tagged value. Having records generated means now you have to worry about global scope of the field names, which we all know can easily conflict. However, local variables are much less concerning, and I'd argue easy to reason about, even though it technically is modifying the environment. Something like:

[jsx|<div>x@{return 1}y@{return 2}</div>|]
-- now both x and y are in scope

It wouldn't require any exotic data structures, indexing, or boilerplate for the client code, and I think it should be easy to ensure that your tags don't conflict with other variable names in the current do block. I just wonder if this is even possible given the way do notation sugar works.

dackerman commented 7 years ago

Oh, and one other thing about syntax: I am hesitant to put the varName@ outside of the curly braces, as it seems likely to conflict with email addresses. I was tentatively using {varName@someExpression}. I suppose we should introduce escaping at least for { and }, but with the former syntax, we'd also need to do this for @.

nmattia commented 7 years ago

Ok, I've added support for more ways of accessing values. I added two functions to showcase it. In all cases the signature of the jsx block is m (HList '[...]) and the signature of the jsx' block is m a, where a is the value you want to get. First multi1:

multi1 :: (MonadWidget t m) => m (Int, String, Double)
multi1 = do
  res <- [jsx|<div>x@{return anInt}<p>y@{return aDouble}</p><a><b>str@{return aString}</b></a></div>|]
  return ( pick res
         , pickProxy (Proxy :: Proxy "str") res
         , pick @"y" res )

The first element is accessed using pick with no annotation. That requires no extension whatsoever (except for QuasiQuotes, of course). The obvious drawback here is that, if there are two values of the same type, you can't specify which one you want. The second element is accessed using a Proxy. The only extension that you need is DataKinds to be able to promote a String ("str") to a Symbol (and QuasiQuotes, of course). Finally the last element is accessed using TypeApplications.

Then there's multi2:

multi2 :: (MonadWidget t m) => m (Int, String, Double)
multi2 = [jsx'|<div>x@{return anInt}<p>y@{return aDouble}</p><a><b>str@{return aString}</b></a></div>|]

That one does not require any extension (except, of course...). However it is restricted to the actual instances you write. However as you can see the instance (for tuples at least) are pretty straightforward (once you understand what Metamorph does) and could be generated with TH. Another drawback is that the description of Metamorph needs UndecidableInstances enabled (in SymIndex.hs). Not sure that's really needed however.

Regarding adding new variables/functions to the scope using TH: it's the first thing I tried, however at some point I got the feeling that it wasn't possible. My gut tells me that it'll be difficult to put those in scope without making them top-level declarations. Don't trust me on this one, I haven't used TH much.

This is all to say that we can get a long way wihout template haskell with simply leveraging the type-system. It'll probably be more maintainable in the long term as well (which reminds me: QQ.hs can be fixed so that users don't need to turn on OverloadedStrings, I've completely failed at updating it).

nmattia commented 7 years ago

Ah, regarding parsing: there's also identifier = { ... } which wouldn't clash with emails (but with some kinds of assignment if there's javascript involved).

dackerman commented 7 years ago

@nmattia so I've thought about this for a while - I don't think the syntactic sugar of do notation is extensible in the way we'd want it to be (such that we can introduce new local names into scope without needing it to be i a structure). I like your idea and am inclined to merge it. Honestly at this early stage of the project we can always change things later without worrying too much.

So, do you want to make a PR for this? Regarding syntax, I think a@{expression} is fine as long as we allow for a\@{expression} or something to escape the naming. I even think it might be a good idea to introduce some text \{not an expression\} along with it, but am less concerned about it since that was an existing issue.

Thanks again for all your efforts!

nmattia commented 7 years ago

Cool, I'll submit a PR with

Regarding a\@{expression} I suggest we only focus, in a different PR, on some text \{not an expression\}. Sounds sensible?

dackerman commented 7 years ago

Sounds good to me! Feel free to update the README as well - I'd be looking for some basic examples of this new functionality, but no need for animated gifs or anything :)

Separate PR for escaping {} is fine by me.

mankyKitty commented 3 years ago

Apologies for the necromancy, @nmattia is it possible for this PR to be submitted, here or my fork? I know it's been a while. Curious about dusting things off. :)

nmattia commented 3 years ago

@mankyKitty oh wow! it's been a while, I don't remember much about this. I'm afraid this PR will never see the light of day 😅