reflex-jsx icon indicating copy to clipboard operation
reflex-jsx copied to clipboard

Questions about reflex-jsx limitations

Open meditans opened this issue 8 years ago • 22 comments

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?

meditans avatar Oct 31 '16 10:10 meditans

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!

dackerman avatar Oct 31 '16 14:10 dackerman

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?

saurabhnanda avatar Nov 04 '16 11:11 saurabhnanda

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

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

instead there is:

<foo onClick={this.someHandler()}>...</foo>

BartAdv avatar Nov 04 '16 12:11 BartAdv

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.

saurabhnanda avatar Nov 04 '16 12:11 saurabhnanda

@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...

BartAdv avatar Nov 04 '16 13:11 BartAdv

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

dackerman avatar Nov 04 '16 14:11 dackerman

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!

meditans avatar Nov 10 '16 08:11 meditans

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?

saurabhnanda avatar Nov 10 '16 09:11 saurabhnanda

@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.

dackerman avatar Nov 10 '16 14:11 dackerman

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!

meditans avatar Nov 11 '16 09:11 meditans

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:

  • Doesn't introduce too much new syntax
  • Doesn't require extra type signatures or too much boilerplate to make work
  • Easy to reason about - i.e. not too "magical"
  • Should provide reasonable error messages

Things I'm not too concerned about:

  • Backward compatibility

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.

dackerman avatar Nov 11 '16 15:11 dackerman

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.

  • Doesn't introduce too much new syntax: doesn't introduce any new syntax. However the pattern-matching/value extraction can get very verbose.
  • Doesn't require extra type signatures or too much boilerplate to make work: Nothing special to do, except for getting that darn value out of the tuple.
  • Easy to reason about - i.e. not too "magical": Not magical at all, pretty caveman-ish.
  • Should provide reasonable error messages: here's what the error messages look like:
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

  • Doesn't introduce too much new syntax: I don't think so, though it's unclear what the implementation would look like.
  • Doesn't require extra type signatures or too much boilerplate to make work: The user needs to pattern match on the Maybe.
  • Easy to reason about - i.e. not too "magical": The user just needs to know how to use lookup :: Map k v -> k -> Maybe v
  • Should provide reasonable error messages: There shouldn't be any new error message

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.

  • Doesn't introduce too much new syntax: User needs to specify the return type and/or type constructor.
  • Doesn't require extra type signatures or too much boilerplate to make work: There is a significant amount of boilerplate.
  • Easy to reason about - i.e. not too "magical": That depends on whether or not you consider TH magical. It can definitely be opaque.
  • Should provide reasonable error messages: I guess it would?

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.

  • Doesn't introduce too much new syntax: Need to enable TypeApplications
  • Doesn't require extra type signatures or too much boilerplate to make work: There is some extra type signature, but it is part of the design. There might be a way to get rid of the two function calls and have something as follows:
ex5 = unWrap @"foo" <$> [jsx|<div>foo@{...}</div>|]
  • Easy to reason about - i.e. not too "magical": This once again depends on what's considered magical. Once the [jsx|...|] block has returned a value we do not rely on TH anymore. However TypeApplications is a pretty recent addition to GHC.
  • Should provide reasonable error messages: The type messages should be straightforward, effectively telling you that "foo" is not contained in ["bar", "baz"].

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 avatar Nov 13 '16 11:11 nmattia

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.

nmattia avatar Nov 14 '16 19:11 nmattia

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

  • What would the type signature of the jsx block itself be?
  • Do you need to enable these extensions in client code?
  • If so, is there a more verbose way to use this without the extensions if someone wanted?
  • What would the syntax look like to grab, say 3 fields at once? Maybe more realistically you'd get the whole jsx value out, then just use (pick @"myField" jsxResult) where you want to use myField?

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 avatar Nov 16 '16 14:11 dackerman

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 @.

dackerman avatar Nov 16 '16 15:11 dackerman

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 avatar Nov 16 '16 21:11 nmattia

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

nmattia avatar Nov 16 '16 21:11 nmattia

@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!

dackerman avatar Nov 19 '16 17:11 dackerman

Cool, I'll submit a PR with

  • updated code
  • updated README (should I leave that to you?)

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

nmattia avatar Nov 21 '16 19:11 nmattia

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.

dackerman avatar Nov 22 '16 03:11 dackerman

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. :)

mankyKitty avatar Nov 16 '20 14:11 mankyKitty

@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 😅

nmattia avatar Nov 30 '20 12:11 nmattia