HeinrichApfelmus / threepenny-gui

GUI framework that uses the web browser as a display.
https://heinrichapfelmus.github.io/threepenny-gui/
Other
441 stars 77 forks source link

Eliminate the TP monad and the MonadTP class #17

Closed HeinrichApfelmus closed 11 years ago

HeinrichApfelmus commented 11 years ago

I would like to streamline the API by removing the TP monad and the MonadTP class.

The TP monad carries around some information about the session. However, the observation is that for functions like appendTo or getValue that operate on a particular element, the session information is essentially already encoded in the element argument. After all, the Element type can be thought of as pointing to one particular element in one particular browser window; so it's not just an "abstract" DOM node, but a specific instance in a particular session. To make a long story short, we can change the type to

getValue :: Element -> IO String

by tracking the session context with every Element.

This does not work for all functions. For instance, the getHead or getElementById functions need to refer to a particular session. However, I think it is natural to add an argument called Window which represents a particular browser window (and associated session context).

getElementById :: Window -> IO (Maybe Element)

I think that these two changes would improve the API syntactically while keeping it conceptually simple.

Please let me know what you think.

fluffynukeit commented 11 years ago

Eliminating the Ji/TP monad sounds like a good idea. I'm often writing MonadTP constraints in my code and I'm not sure really sure why. I guess there is flexibility to implement your own monadic behaviors on top of the standard monad?

Could you elaborate on your proposed change a bit more? For instance, would the Element type change or would it remain a string?

My first impression is that this looks to make things more complicated. With the monad implementation, both getValue and getElementById work similarly - there is no need to draw a distinction between the two by having one take a Window argument and another one not. It sounds like in your proposal, functions will be split into two different groups.

Where do the Windows come from? Can I query them from an Element? If so, does that mean I either need to have an Element in scope in order to get a Window or manage the Windows myself?

Just throwing out questions. Heightened scrutiny is my style of review; repeatedly poke ideas to make sure they hold up. :)

HeinrichApfelmus commented 11 years ago

Yep, the implementation of the Element type would become more elaborate and include session context. But since this is an abstract data type, the change would be transparent for the library user.

The Window is meant to represent the client / browser window. Maybe it should be called Document, that would be the standard DOM concept. The idea is that threepenny passes the browser window whenever it starts a new session. This way, you can query various elements and so on when setting up the GUI. Example:

main = serve $ Config { ... tpWorker = \w -> setup w >> handleEvents ... }

setup :: Window -> IO ()
setup window = do
    body    <- getBody window
    e1      <- new #+ body
    e2      <- new #. "foo" #+ e1
    Just e3 <- getElementById window "bar"
    ...

I think it's quite natural, it's essentially the document part in document.getElementById. It's also common in other GUI frameworks like wxHaskell or GTK.

One nice touch about making the session context / browser window explicit is that we can now talk about multiple browser windows. This would make it easier to implement things like the multi-user chat example.


I like the idea of getting a window from an Element, this can be useful if we don't want to keep the window in scope all the time.

parent :: Element -> IO Window

Hm, but there is one problem I see. Namely, Elements can exist before they are attached to a particular window. But that seems to be on semantically dubious grounds anyway. What does the following snippet mean?

div <- new #. "foo"
div #+ body
div #+ body
onClick div ...

This appends a single div element twice to the body and registers an event handler on it. Does the event handler work on both now? Just one? What if we set the HTML context of div, will that change the document in two places?

EDIT: I have done some tests with JavaScript, and it appears that the browser implements the following semantics: appending an existing element to some node in the DOM will actually remove the element from its previous position! This is somewhat unexpected, but makes sense in that every element has a unique identity. If you want to insert an element twice, you have to copy it first.

fluffynukeit commented 11 years ago

I think it's a redesign we should try. The multiple browser windows advantage makes sense to me. Opening new popup browser windows is attractive. I can see users needing that kind of thing. Let's try to iron out the wrinkles.

In your example above, is it true then that each browser window has its own session? My understanding is that each session is sandboxed, and if that's the case then I don't see how you can get multiple windows to interact with each other in a straight forward way. I guess one way to do it is have a standard root Window that is passed at the start of the session, and then spawn new Windows as desired. Something like

setup :: Window -> IO ()
setup window = do
    body <- getBody window
    el <- new #+ body
    onClick el $ \(EventData a) -> do 
        sWindow <- makeSubWindow window
        populatePopupBody sWindow

Bonus points if makeSubWindow generates the popup automatically. Technically each window/subwindow would still have its own session, but they could interact by having Elements (and their sessions) from other Windows within scope on the server code.

HeinrichApfelmus commented 11 years ago

Yes, essentially the notion of "session" is entirely replaced by the notion of "browser window", which I find very tangible.

The server can access all browser windows that are in scope. For instance, the following code changes the value in one window when the user clicks in a different one.

setup :: Window -> IO ()
setup window = do
    w1 <- makeSubWindow window
    w2 <- makeSubWindow window

    body1 <- getBody w1
    e1    <- new #+ body1

    body2 <- getBody w2
    e2    <- new #+ body2

    onClick e1 $ \(EventData a) -> do 
        setValue e2 =<< getValue e1

If it's in scope, you can use it.


I do have to mention that these multi-user/-window use cases are not my main motivation, though. Rather, I'm interested in using the plain IO monad because that makes another modification -- event handlers -- I have in mind much easier.

fluffynukeit commented 11 years ago

Ok, sounds good. I think we should move forward. How can I help?

One thing I would like to avoid doing is guiding the development of threepenny-gui so that it more easily fits into the FRP scene (obviously reactive-banana being the primary candidate). I think any proposed redesigns should stand on their own merits, with the goal of making GUI's on Haskell natural and approachable. If in doing this, TPG becomes a better match with FRP, that's great but I hope incidental. That said, I do look forward to learning more about your event handler ideas because I am finding working on TPG very educational so far.

HeinrichApfelmus commented 11 years ago

Ok, I have implemented the fairly mechanical change of adding session information to the Element type and removing corresponding mentions of TP and MonadTP. Only the core modules so far, so no tests yet whether everything still works.

I completely agree that threepenny-gui should be able to stand by itself. The changes I have in mind (AddHandler) are of course informed by FRP, but they are intended to improve usability in the imperative style. I'll open an issue for discussion soon.

HeinrichApfelmus commented 11 years ago

The TP did make a return in the form of the Dom monad, but the purpose of the latter is now restricted to element creation. In contrast, manipulation of existing elements can be done in the IO monad as usual. I'm still happy with the overall result, so this issue can be closed for now.