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

alternative monadic API: concrete example with code #8

Closed rvion closed 7 years ago

rvion commented 7 years ago

based on https://github.com/OutWatch/purescript-outwatch/issues/7 and https://github.com/OutWatch/purescript-outwatch/issues/3, I gave a try to some alternative monadic API

📝 operators like ==>, <==, and := are gone 📝 no more [,,,] lists neither

it seems to works well. the only thing missing seems to be some

instance childReceiverBuilder2 :: ReceiverBuilder ChildStreamReceiverBuilder (Observable (VDomBuilder e)) e  where
instance childrenStreamReceiverBuilder2 :: (Traversable t) => ReceiverBuilder ChildrenStreamReceiverBuilder (Observable (t (VDomBuilder e))) e where

so components can still create dynamically subcomponents with personal handlers for inner state management.

@LukaJCB how does it look ?

module Main where

-- import OutWatch
import Prelude
import OutWatch.Sink as Sink
import RxJS.Observable as Observable
import Builder (class AttributeBuilder, class ReceiverBuilder)
import Control.Alt (map)
import Control.Monad.Aff.Console (CONSOLE)
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (log, logShow)
import Control.Monad.RWS.Trans (lift)
import Control.Monad.State (class MonadState, State, StateT(..), execState, execStateT, modify)
import DOM.Node.Types (Element)
import Data.Array (cons, snoc, replicate)
import Data.Array.Partial (head)
import Data.CommutativeRing ((+))
import Data.EuclideanRing ((/))
import Data.Int (round)
import Data.Monoid ((<>))
import Data.Unit (Unit)
import EmitterBuilder (class EmitterBuilder, emitFrom)
import OutWatch (children, createHandler, li, render, src, ul, valueShow, (==>))
import OutWatch (childShow, createNumberHandler, div, img, (:=))
import OutWatch.Attributes (inputNumber, insert, max, text, tpe, (<==))
import OutWatch.Sink (Handler, Observer(..), SinkLike, create)
import OutWatch.Tags (br, hr, input)
import Partial.Unsafe (unsafePartial)
import R.Yolo (x)
import RxJS.Observable (Observable, combineLatest, interval, startWith)
import VDomModifier (VDom)

main :: Eff _ Unit
main = do 
    log "start"
    -- render "#app" app
    (unsafeFirst <$> build test) >>= render "#app"
    log "done"

test2 :: forall e. Number -> VDomBuilder e Unit
test2 max = do
    let def = 0.0
    h <- createHandlerE [def]
    let imageLists = h.src # map (\n -> replicate (round n) (img[src := "img.svg"]))        
    ul_ do
        input_ do
            type_ "range"
            inputNumber_ h
            valueShow_ def
            max_ max
        div_ (children_ imageLists)
        div_ (childShow_ h.src)

test :: forall e. VDomBuilder e Unit
test = ul_ do
    h <- createHandlerE [2.0]
    input_ do
        type_ "number"
        inputNumber_ h

    hr_ (pure unit)
    li_ do
        text_ "the cat is  "
        text_ "nice"
    li_ (text_ "yay")
    li_ do 
        text_ "sub elements"
        ul_ do
            li_ (text_ "ok")
            li_ (text_ "ok")
            li_ (test2 1.0)
            li_ (test2 3.0)

----------------------------------------------------------------------------
---- types -----------------------------------------------------------------

foreign import data H :: !
type VDomBuilder e a = StateT (Array (VDom e)) (Eff (h::H|e)) a
type NodeBuilder e = VDomBuilder e Unit -> VDomBuilder e Unit

----------------------------------------------------------------------------
-- Handlers, Observables, Sinks --------------------------------------------

createHandlerE :: forall a e. Array a -> VDomBuilder e (Handler e a)
createHandlerE a = lift $ pure (createHandler a)

----------------------------------------------------------------------------
-- Elements ----------------------------------------------------------------

wrapElem :: forall e. (Array (VDom e) -> (VDom e)) -> VDomBuilder e Unit -> VDomBuilder e Unit
wrapElem e b = lift (e <$> execStateT b []) >>= push 

ul_ :: forall e. NodeBuilder e
ul_ = wrapElem ul

li_ :: forall e. NodeBuilder e
li_ = wrapElem li

div_ :: forall e. NodeBuilder e
div_ = wrapElem div

br_ = wrapElem br
hr_ = wrapElem hr

input_ :: forall e. NodeBuilder e
input_ = wrapElem input

text_ :: forall e. String -> VDomBuilder e Unit
text_ t =  push (text t)

-- ul_1 :: forall e. VDomBuilder e Unit -> VDomBuilder e Unit
-- ul_1 b = lift (ul <$> execStateT b []) >>= push

----------------------------------------------------------------------------
-- Attributes --------------------------------------------------------------

wrapAttribyte :: 
    forall builder value e. 
    AttributeBuilder builder value =>  
    builder ->
    value ->
    VDomBuilder e Unit
wrapAttribyte b v = push (b := v)

type_ :: forall e. String -> VDomBuilder e Unit
type_ = wrapAttribyte tpe

valueShow_ :: forall s e. Show s => s -> VDomBuilder e Unit
valueShow_ = wrapAttribyte valueShow

max_ :: forall e. Number -> VDomBuilder e Unit
max_ = wrapAttribyte max

----------------------------------------------------------------------------
-- Emitter -----------------------------------------------------------------

wrapEmitter :: forall builder a e r.       
  EmitterBuilder builder a e => 
  builder -> 
  SinkLike e a r ->
  VDomBuilder e Unit
wrapEmitter b s = push (b ==> s)

-- inputNumber_ :: forall e. Number -> VDomBuilder e Unit
inputNumber_ = wrapEmitter inputNumber

----------------------------------------------------------------------------
-- Receiver ----------------------------------------------------------------

wrapReceiver :: forall builder stream e.
  ReceiverBuilder builder stream e => 
  builder -> 
  stream ->
  VDomBuilder e Unit
wrapReceiver b s = push (b <== s)

children_ :: forall e. Observable (Array (VDom e)) -> VDomBuilder e Unit
children_ s = push (children <== s)

-- instance childReceiverBuilder :: ReceiverBuilder ChildStreamReceiverBuilder (Observable (VDom e)) e  where
--   bindFrom builder obs =
--     let valueStream = map modifierToVNode obs
--     in Receiver (ChildStreamReceiver valueStream)

-- children_2 :: forall e. Observable (VDomBuilder e Unit) -> VDomBuilder e Unit
-- children_2 = do 
--     children

childShow_ :: forall a e. Show a => Observable a -> VDomBuilder e Unit
childShow_ = wrapReceiver childShow
-- ReceiverBuilder builder stream eff | stream -> eff, builder -> stream where
--   bindFrom :: builder -> stream -> VDom eff

----------------------------------------------------------------------------
-- Helpers -----------------------------------------------------------------

push :: forall vdom m. (MonadState (Array vdom) m) => vdom -> m Unit
push = (\e l -> snoc l e) >>> modify

unsafeFirst :: forall a. Array a -> a 
unsafeFirst a = unsafePartial (head a)

build :: forall e a. VDomBuilder e a -> Eff (h::H|e) (Array (VDom e))
build b = execStateT b []

------------------------------------------------------------------------------
---- old ---------------------------------------------------------------------

app :: forall e. VDom ( console :: CONSOLE | e )
app = div 
    [ root 1.0
    , root 12.0
    , root 100.0
    ]

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

sliderEvents :: forall e. Handler e Number
sliderEvents = createHandler [0.0]
rvion commented 7 years ago

here, children_ deals with (Observable (VDom e)) instead of (Observable (VDomBuilder e)) because some instance is missing. (I don't think this instance has to be complex or slightly magical, but I don't think I could have defined it outside of the library)

rvion commented 7 years ago

there is some more low hanging fruits to harvest here:

here:

type VDomBuilder e a = StateT (Array (VDom e)) (Eff (h::H|e)) a
type NodeBuilder e = VDomBuilder e Unit -> VDomBuilder e Unit
rvion commented 7 years ago

below is a copy-paste of https://github.com/OutWatch/purescript-outwatch/issues/3#issuecomment-289284055 (I copied it here because I think it may be more relevant in this thread)


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)

LukaJCB commented 7 years ago

Kudos to you for putting in such a large amount of effort! I really appreciate it! I'm gonna need some time to parse all of this and check the viability and weigh it against other options.

Few thoughts: I really like the idea of having a monadic api, but also have to think about if it might be too complex for beginners or how well it translates to Scala (I'd like to have near-feature-parity)
I think that if we go through with this kind of Api VDomBuilder should become the new VDom as it'd be the type that gets exposed in the api, whereas the current VDom would only be an implementation detail, so it could be renamed to something more "verbose". Another thing I need to think about, is how we'd implement the Store pattern, that a lot users seem to really enjoy.

That's all for now, I'm sure over the coming weeks (there's a lot of other things on the roadmap) I'll be gaining more insight and will continue to update you :)

rvion commented 7 years ago

Kudos to you for putting in such a large amount of effort! I really appreciate it!

thank you :) I also really appreciate to receive feedback too !

I really like the idea of having a monadic api, but also have to think about if it might be too complex for beginners or how well it translates to Scala (I'd like to have near-feature-parity)

I tweaked a few things trying to reduce the complexity. I think I achieved something as easy as the current version using a newtype, no other type alias, and shorter type names.

From what I saw of scala, I think scala could have the same api without much problems

I think that if we go through with this kind of Api VDomBuilder should become the new VDom as it'd be the type that gets exposed in the api, whereas the current VDom would only be an implementation detail, so it could be renamed to something more "verbose".

I completely agree.

Another thing I need to think about, is how we'd implement the Store pattern, that a lot users seem to really enjoy.

🍏 the store pattern becomes directly available with a monadic api, because the monad can provide some global stores shared amongst components


I also discovered some nice byproducts of using a monadic api (not geting in the way of beginners, but just available for more complex things): some VDomBuilder e attribute setter could inspect the list of attributes and adapt its behaviour, etc.

rvion commented 7 years ago

2 small precisions:

1 - So far, I have found the monadic API more pleasant, correct, and powerfull, but I haven't played enough to weight the cons correctly, so don't take this arguments too seriously :)

2 - OutWatch simplicity is something I loved at first sight, and something I also really care about.

LukaJCB commented 7 years ago

So I've bottled it down to three realistic solutions, I think:

  1. Let createHandler or an Equivalent return an Eff (rx::RX) (Observable, EventToken), where you'd have to map over the Eff and then let the DOM dsl work on the Effs instead of the actual values (There could be some Monad Transformer to help reduce BoilerPlate like ObservableT or something like that)
  2. Use a Monad Api like you described, this would require a quite large change all around.
  3. Return Observables from Components and let them take Observables of VDom, this is more or less a copy of how Cycle.js does it and it works pretty well for them, however this would require even larger changes.

And of course there's 4. Don't change the current API at all and acknowledge it's impureness. Maybe change the Signature of createHandlerImpl to return an Eff and use unsafePerformEff in createHandler

I don't really have a huge preference right now, would probably have to build some example apps in each (which could take a while) :)

rvion commented 7 years ago

thanks for the feedback !

I've just shared what I experimented with over the last days here: https://github.com/OutWatch/purescript-outwatch/pull/9

it's not even half done, but I'm just sharing in case you have some feedback. I'll understand if you don't like the fact I did so many name change. I'll try to explain what I have in mind soon :)

LukaJCB commented 7 years ago

@rvion If you're still interested I made #14 which I hope will be the single future of OutWatch both in Scala and PureScript :)