outwatch / purescript-outwatch

A functional and reactive UI framework based on Rx and VirtualDom
https://outwatch.github.io/?lang=purescript
Apache License 2.0
34 stars 5 forks source link

createNumberHandler, createSink, etc should be Effectfull functions #3

Closed rvion closed 7 years ago

rvion commented 7 years ago

From what I understand, as of now, it's only working because of purescript lack of optimisation. both inlining and caching should break the code.

Am I missing something ? edit: Is there something related to strictness I'm not understanding here ?

should it be something along the lines of:

createHandler :: forall a e. Array a -> Eff (rx :: RX) (Handler e a) 
rvion commented 7 years ago

basically, if I want to spawn 3 components

app :: forall e. VDom e
app = div 
    [ root 1.0
    , root 12.0
    , root 100.0
    ]

the 2 versions below do not behave identically, according to where the sliderEvents is declared, and according to the amout of optimisation purescript is doing (🔴 using something like rollup-plugin-purs will change the behaviour.)

version 1: all sliders share the same variable

root :: forall e. Number -> VDom e
root a = ul
    [ input
        [ tpe := "range"
        , inputNumber ==> sliderEvents
        , max := a
        ]
    , div [children <== imageLists]
    ]
    where
        imageLists = sliderEvents.src # map (\n -> replicate (round n) (img[src := "img.svg"]))

sliderEvents :: forall e. Handler e Number     -- <========== outside of the where clause
sliderEvents = createNumberHandler []

version 2: all sliders are independent, but only because there is not much optimisation.

root :: forall e. Number -> VDom e
root a = ul
    [ input
        [ tpe := "range"
        , inputNumber ==> sliderEvents
        , max := a
        ]
    , div [children <== imageLists]
    ]
    where
        imageLists = sliderEvents.src # map (\n -> replicate (round n) (img[src := "img.svg"]))

        sliderEvents :: forall e. Handler e Number     -- <========== outside of the where clause
        sliderEvents = createNumberHandler []
rvion commented 7 years ago

📝 keeping unsafeCreateXxxHandler functions available would be a good idea, though. once inlining will be mainstream, NoInlinePragma will probably be too. Since thread safety is not always important, being able to quickly define global top level handlers may come handy in lots of situations

rvion commented 7 years ago

From what I understand, creating handlers during instantiation is needed for various design paterns, such as for components to be able to manage an inner state.

So, the problem with making createHandler effectful is that it forces the whole widget creation to be effectful.

instead of

root :: forall e. Number -> VDom (console :: CONSOLE |e)
root a = ul
    [ input
        [ tpe := "range"
        , inputNumber ==> sliderEvents
        , valueShow := 0
        , max := a
        ]
    , div [children <== imageLists]
    ]
    where
        imageLists = sliderEvents.src # map (\n -> replicate (round n) (img[src := "img.svg"]))
        sliderEvents = createNumberHandler []

it would mean writing component definition in some VDomBuilder monad like that:

pseudocode example 1:

root :: forall e. Number -> VDomBuilder e
root max = do 
    sliderEvents <- createHandler
    let imageLists = sliderEvents.src # map (\n -> replicate (round n) (img[src := "img.svg"]))
    pure $ ul
        [ input
            [ tpe := "range"
            , inputNumber ==> sliderEvents
            , valueShow := 0
            , max := a
            ]
        , div [children <== imageLists]
        ]

pseudocode example 2 (taking some inspiration from haskell libraries like Blaze, Lucid, bytestring-builder, etc., and pushing the builder pattern further):

root :: forall e. Number -> VDomBuilder e
root max = do 
    sliderEvents <- createHandler
    let imageLists = sliderEvents.src # map (\n -> replicate (round n) (img[src := "img.svg"]))
    ul_ do
        input_ do
            tpe_ "range"
            inputNumber_ sliderEvents
            valueShow_ 0
            max_ max
        div_ $ children_ imageLists

📝 I think it really fits the builder pattern. 📝 also, when writing a presenter component that only use its parameters for input and handlers, everything remains compatible, the only thing needed is a function to convert a pure VDom e to a VDomBuilder e (probably pure since VDomBuilder would be a monad)

Eventually, the only thing remaining woud be a new render function

render :: forall e. String -> VDomBuilder e -> Eff (vdom :: VDOM | e) Unit

@LukaJCB @sectore : does it make sense to you ?

LukaJCB commented 7 years ago

Hi @rvion, it's indeed something that I've been trying to improve in the last few months. I've been thinking and experimenting a lot and I think longer term, I'd like to move away from Handlers all together in favor of a "pull-only" Api, where you never get access to a Handler, but instead only get an Observable.
I'm still in the process of weighing the pros and the cons of such a solution, but I think it'd be preferable to what we have right now. Your solution is also really interesting, I'll have to see how it stacks against other solutions, I really really appreciate your time and your effort that went into this. I initially designed this API for Scala, where referential transparency is not as big an issue. It's also heavily influenced by pre-0.17 Elm, where you also have the problem that the mailbox function is not referentially transparent. They removed that API though, and I'd also like to get rid of it in the long term.

rvion commented 7 years ago

@LukaJCB great to hear the project is alive !! :) I spent most of the night thinking about this problem.

here is what I came up with https://github.com/OutWatch/purescript-outwatch/issues/8

I tried various different designs: this one is best one I found so far based on all constrainsts I gave myself:

I didn't test it extensively, so there might be important problems I did not see

📝 as a bonus, the monadic api could allow user to suply their own monadtransformers with some reader monad providing configuration, or globally available message bus via handlers. Halogen permit this, and this is from what I saw very usefull (I played with elm pre 0.17 and post 0.17, pux, halogen and various other UI libs, and the pure functional paradigm quickly becomes annoying without this. Here, it comes for free, at almost no cost)

rvion commented 7 years ago

Here is a small visual example (nothing new)

module Example.Monadic.Counter where

import Control.Monad.Eff.Random (RANDOM, randomInt)
import Control.Monad.Except.Trans (lift)
import OutWatch.Monadic.Attributes (childShow_, click_, text_)
import OutWatch.Monadic.Tags (button_, div_, h3_)
import OutWatch.Monadic.Types (HTML)
import OutWatch.Monadic.Utils (createHandler_, cmapSink)
import OutWatch.Pure.Sink (createHandler)
import Prelude (bind, Unit, (+), (#), const, negate, ($), show)
import RxJS.Observable (merge, scan, startWith)

version 1

incrementHandlder = createHandler [0]
decrementHandlder = createHandler [0]
count = merge incrementHandlder.src decrementHandlder.src
    # scan (+) 0
    # startWith 0

app :: forall e. HTML (random :: RANDOM|e) Unit
app = do
    rnd <- lift $ randomInt 10 20
    div_ do
        div_ do 
            text_ "some random number:"
            text_ (show rnd)
        button_ do
            text_ "Decrement"
            click_ (decrementHandlder # cmapSink (const (-2)))
        button_ do
            text_ "Increment"
            click_ (incrementHandlder # cmapSink (const ( 2)))
        h3_ do
            text_ "Monadic Counter: "
            childShow_ count

when I render the widget twice:

external_bindings

version 2

app :: forall e. HTML (random :: RANDOM|e) Unit
app = do
    rnd <- lift $ randomInt 10 20
    let incrementHandlder = createHandler [0]
    let decrementHandlder = createHandler [0]
    let count = merge incrementHandlder.src decrementHandlder.src
            # scan (+) 0
            # startWith 0
    div_ do
        div_ do 
            text_ "some random number:"
            text_ (show rnd)
        button_ do
            text_ "Decrement"
            click_ (decrementHandlder # cmapSink (const (-2)))
        button_ do
            text_ "Increment"
            click_ (incrementHandlder # cmapSink (const ( 2)))
        h3_ do
            text_ "Monadic Counter: "
            childShow_ count

when I render the widget twice:

with_let