New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Halogen Components #138

Closed
jdegoes opened this Issue Jun 9, 2015 · 49 comments

Comments

5 participants
@jdegoes
Copy link
Contributor

jdegoes commented Jun 9, 2015

Introduction

Halogen components currently suffer from a variety of drawbacks:

  • Queries. It is not possible to extract information from components. Since components close over their state, this means it is not possible to read component state. For foreign components, it is not possible to effectfully extract foreign state.
  • Localization. Every detail about even the smallest component somewhere deep inside the application must propagate all the way to the top, even for pure components. Parent and child components cannot communicate in a principled or localized way.
  • Invariance. Components are invariant in their input type: they must accept the same type they produce in response to user input (at least in practice, if not in theory). This leads to a single type representing commands, requests for information, and responses. As a result, the types alone do not precisely model information flow, and the types permit many undesirable flows with poorly-defined semantics.

To work around the first two issues, the emerging anti-pattern involves passing around Driver so that components may, on user input, effectfully inject information into the top of the program, where it can then trickle down to where it is required. This effectful style is difficult to reason about and leads to tangled programs.

This proposal introduces a new design for components, based on many iterations and much discussion, which is not yet fully realized, but which has the ability to address all these drawbacks while still permitting classic Halogen components and combinators as a special case of a more generic abstraction.

Component

newtype Component s f g p = Component 
  { render  :: s -> HTML p (f Unit),
    query   :: forall i. Free f i -> StateT s g i }

instance functorComponent :: Functor (Component s f g)

A component is a bundle of two functions:

  1. render — This is a pure function that renders the component to HTML based on the component's state. Unlike prior Halogen components, this rendering function may not perform any effects on user input.
  2. query — This is a function to query the component, which may or may not modify the state or have other effects up to g.

The type parameters have the following meanings:

  • s - The type of the component state. A component is invariant in s.
  • f - The component's query algebra, whose interpretation has effects up to StateT s g.
  • g - The monad describing the component's extra (non-State) effects.
  • p - The type of placeholders in the component, which can be thought of as addresses for child components that will be installed into the HTML of the parent component. Components form a Functor in p, allowing easy manipulation of placeholders.

Rendering

A component can render itself, given the state. As part of this rendering, it may specify the event listeners to be called on user input. These event listeners may produce commands to the query DSL accepted by the component.

By design, commands generated from user input return Unit, because the HTML rendering must neither know nor care what the query processor does with them. Rather, the HTML rendering must always be strictly a function of component state.

User input may generate only one command per user input event. This requirement could be loosened but only by allowing the HTML rendering to interact with the query processor.

Querying

A component also exposes a query DSL, which is a Free algebra in f. Classic Halogen components consist purely of "write-only" commands, which return Unit, and which are designed to be called in response to user-input.

The query DSL is implemented through a query processor, which is a function that lifts the query DSL into a monad g, with stateful effects on the component's state layered on using StateT. Thus, the query processor may implement the query DSL using both the component state (reads and writes), as well as additional effects captured by g.

Composition

Child components can be installed into parent components. Parent components can communicate with their children, as well as all descendants. Sibling components can communicate only through the facilitation of a parent component.

This communication architecture ensures that parent components remain in control of information flow and promotes modular architecture.

Asynchronous Behavior

By design, components cannot block rendering, because they are split into pure rendering and possibly effectful query processing parts.

In addition, there is no easy way for race conditions to accidentally overwrite state, because all query processing occurs in a StateT monad, and query processors do not have direct access to state (and therefore cannot easily capture over it). This does not prevent such conditions from occurring, but it does make it much harder for a user to accidentally squash state.

Pure Component

Pure components are simply components whose effects are limited to component state.

type ComponentPure s f p = forall g. (Monad g) => Component s f g p

Classic Halogen Components

Classic Halogen components are simply a special case of normal components, where all inputs in the query DSL are commands that return Unit.

module ClickComponent where
  import Control.Monad.State.Trans
  import Control.Monad.State.Class(modify)
  import Control.Monad.Free
  import Control.Monad.Rec.Class

  import Data.Inject
  import Data.Void
  import Data.Identity

  import Halogen  

  data Input a = ClickIncrement a | ClickDecrement a

  instance functorMyComponent :: Functor Input where
    (<$>) f (ClickIncrement a) = ClickIncrement (f a)
    (<$>) f (ClickDecrement a) = ClickDecrement (f a)

  clickIncrement :: forall g. (Functor g, Inject Input g) => Free g Unit
  clickIncrement = liftF (inj (ClickIncrement unit) :: g Unit)

  clickDecrement :: forall g. (Functor g, Inject Input g) => Free g Unit
  clickDecrement = liftF (inj (ClickDecrement unit) :: g Unit)

  counterComponent :: forall g. (MonadRec g) => Component Number Input g Void
  counterComponent = Component { render : render, query : query }
    where
      eval :: forall g a. (Monad g) => Input (Free Input a) -> StateT Number g (Free Input a)
      eval (ClickIncrement next) = do
        modify (+1)
        return next
      eval (ClickDecrement next) = do
        modify (flip (-) 1)
        return next

      render :: Number -> HTML Void (Input Unit)
      render n = todo

      query :: forall g i. (MonadRec g) => Free Input i -> StateT Number g i
      query = runFreeM eval

  test :: Free Input Unit
  test = do
    clickIncrement
    clickIncrement
    clickDecrement

By design, the commands that can be sent to the component (clickIncrement, and clickDecrement) work in any Free algebra which contains Input (including, of course, Input itself).

Foreign Components

module EditorComponent where
  import Control.Monad.State.Trans
  import Control.Monad.State.Class(modify)
  import Control.Monad.Free
  import Control.Monad.Rec.Class
  import Control.Monad.Eff
  import Control.Monad.Trans(lift)
  import Control.Apply((*>))

  import Data.Inject
  import Data.Void
  import Data.Identity

  import Halogen  

  data Input a = GetContent (String -> a) | SetContent a String | GetCursor (Number -> a)

  instance functorInput :: Functor Input where
    (<$>) = todo

  getContent :: forall g. (Functor g, Inject Input g) => Free g String
  getContent = liftF (inj (GetContent id) :: g String)

  setContent :: forall g. (Functor g, Inject Input g) => String -> Free g Unit
  setContent s = liftF (inj (SetContent unit s) :: g Unit)

  getCursor :: forall g. (Functor g, Inject Input g) => Free g Number
  getCursor = liftF (inj (GetCursor id) :: g Number)

  editorComponent :: forall eff. Component Unit Input (Eff (dom :: DOM | eff)) Void
  editorComponent = Component { render : render, query : query }
    where 
      eval :: forall eff a. Input (Free Input a) -> StateT Unit (Eff (dom :: DOM | eff)) (Free Input a)
      eval (GetContent f  ) = lift $       f <$> effectfulGetContent
      eval (SetContent n s) = lift $ const n <$> effectfulSetContent s
      eval (GetCursor  f  ) = lift $       f <$> effectfulGetCursor

      render s = todo

      query :: forall eff i. Free Input i -> StateT Unit (Eff (dom :: DOM | eff)) i
      query = runFreeM eval

  test :: Free Input Unit
  test = do
    cursor  <- getCursor
    content <- getContent
    setContent $ content ++ (show cursor)

  foreign import data DOM :: !

  foreign import effectfulGetContent :: forall eff. Eff (dom :: DOM | eff) String
  foreign import effectfulSetContent :: forall eff. String -> Eff (dom :: DOM | eff) Unit
  foreign import effectfulGetCursor  :: forall eff. Eff (dom :: DOM | eff) Number

Composing Components

The install family of functions can be used to install a child component into a parent component at well-defined places inside the HTML of the parent component.

  installR :: forall s f g pl pr s' f' p'. (Ord pr, Plus g) =>
    Component s f (QueryT s' f' pr p' g s) (Either pl pr) ->   -- parent
    (pr -> Tuple s' (Component s' f' g p'))               ->   -- factory
    Component (InstalledState s s' f' g pl p') (Coproduct f (ChildF pr f')) g (Either pl p')
  installR a f = todo

  installL :: forall s f g pl pr s' f' p'. (Ord pl, Plus g) =>
    Component s f (QueryT s' f' pl p' g s) (Either pl pr) ->   -- parent
    (pl -> Tuple s' (Component s' f' g p'))               ->   -- factory
    Component (InstalledState s s' f' g pr p') (Coproduct f (ChildF pl f')) g (Either pr p')
  installL a f = todo

  installAll :: forall s f g p s' f' p'. (Ord pr, Plus g) =>
    Component s f (QueryT s' f' p p' g s) p               ->   -- parent
    (p -> Tuple s' (Component s' f' g p'))                ->   -- factory
    Component (InstalledState s s' f' g p p') (Coproduct f (ChildF p f')) g p'
  installAll a f = todo

  type ComponentState s f g p = Tuple s (Component s f g p)

  data ChildF p f i = ChildF p (f i)

  type ComponentState s f g p = Tuple s (Component s f g p)

  type InstalledState s s' f' g p p' = 
    { parent   :: s, 
      children :: Map.Map p (ComponentState s' f' g p'), 
      factory  :: p -> ComponentState s' f' g p' }

  data QueryT s' f' p p' g s a = QueryT (StateT (InstalledState s s' f' g p p') g a)

The installation happens automatically in response to changes in the parent component's HTML. Thus, the number and locations of the child components may change dynamically in response to user input or queries to the parent component.

The output monad of every parent component is QueryT for some base monad g. This monad transformer allows the parent to manipulate its own state, and send and received typed messages to its children. After installation using installR, installL or installAll, the communication between parent and child, as well as the children placeholders, are completely hidden, leaving only a simple component with composite state, composite query DSL, and "leftover" placeholders.

  -- queries a particular child from the parent:
  query :: forall s s' f' p p' g. p -> (forall i. Free f' i -> QueryT s' f' p p' g s (Maybe i))
  query p q = todo

  -- lifts an effect into the QueryT monad:
  effect :: forall s' f' p p' g s a. (Monad g) => g a -> QueryT s' f' p p' g s a
  effect ga = todo

  -- MonadState for QueryT so parents can manipulate their own state
  instance monadStateQueryT :: MonadState s (QueryT s' f' p p' g s) where
    state f = todo

  instance functorQueryT :: Functor (QueryT s' f' p p' g s) where
    (<$>) f fa = todo

  instance applyQueryT :: Apply (QueryT s' f' p p' g s) where
    (<*>) f fa = todo

  instance applicativeQueryT :: Applicative (QueryT s' f' p p' g s) where
    pure a = todo

  instance bindQueryT :: Bind (QueryT s' f' p p' g s) where
    (>>=) fa f = todo

  instance monadQueryT :: Monad (QueryT s' f' p p' g s)

  instance functorChildF :: (Functor f) => Functor (ChildF p f) where
    (<$>) f (ChildF p fi) = ChildF p (f <$> fi)

Two functions query and effect, as well as standard instances for QueryT, may be used to easily build parent component query processors:

  test = do 
    state <- get
    effect $ doEffect
    set state
    resp <- query editor getContent
    return $ fromMaybe "" resp

Running Components

To run a component, you provide the component and an initial state.

The component has an arbitrary input DSL f, and runs effects in Aff. By design, components cannot ever block rendering.

  type Driver f eff = forall i. f i -> Aff (HalogenEffects eff) i

  runComponent :: forall eff s f. (Functor f) =>
    Component s f (Aff (HalogenEffects eff)) Void ->
    s                                             ->
    Aff (HalogenEffects eff) (Tuple HTMLElement (Driver f eff))
  runComponent c s = todo
@puffnfresh

This comment has been minimized.

Copy link
Contributor

puffnfresh commented Jun 9, 2015

What I think:

newtype Component f s p q r = Component 
  { render :: s -> HTML p q,
    query  :: q -> StateT s f r }
@jdegoes

This comment has been minimized.

Copy link
Contributor

jdegoes commented Jun 9, 2015

We discussed, problem with Component f s p q r is that the DSL becomes untyped, i.e. query request is not connected to query response. Whereas if you use the alternate, higher-kinded formulation, you can guarantee that a request f i will yield a value of type i, which is a very nice property to have.

@jdegoes jdegoes changed the title Further development of component ideas New Halogen Components Jun 15, 2015

@jdegoes

This comment has been minimized.

Copy link
Contributor

jdegoes commented Jun 15, 2015

@puffnfresh Updated.

@garyb

This comment has been minimized.

Copy link
Member

garyb commented Jun 23, 2015

What's the status with this, just out of interest?

@jdegoes

This comment has been minimized.

Copy link
Contributor

jdegoes commented Jun 23, 2015

@garyb It's all ready for review, which is going to fall to you and @cryogenian now.

This proposal would change HTML slightly (adding back p, which can be ignored by code that doesn't need it), and would completely change Component. SF would probably go away since it can't be compiled to Component (or we could leave it as an alternative to Component).

Right now I'd be particularly interested in use cases, to make sure this design can adequately handle all the ugly cases we run into now.

Also due for a review is Widget et al just to see if we could simplify that with the new definition of Component, runUI, etc.

@garyb

This comment has been minimized.

Copy link
Member

garyb commented Jun 23, 2015

Is there a problem with using FreeC/Coyoneda? I thought it was previously mentioned in here.

Types seem to work out:

module Halogen where

  import Control.Monad.Free
  import Control.Monad.State.Trans
  import qualified Halogen.HTML as H

  type HTML p a = H.HTML a

  newtype Component s f g p = Component
    { render :: s -> HTML p (f Unit)
    , query  :: forall i. FreeC f i -> StateT s g i
    }

module ClickComponent where

  import Control.Monad.State.Trans
  import Control.Monad.State.Class (modify)
  import Control.Monad.Free
  import Control.Monad.Rec.Class

  import Data.Coyoneda
  import Data.Inject
  import Data.Void
  import Data.Identity

  import Halogen
  import qualified Halogen.HTML as H

  data Input a = ClickIncrement a | ClickDecrement a

  clickIncrement :: forall g. (Inject Input g) => FreeC g Unit
  clickIncrement = liftFC (inj (ClickIncrement unit) :: g Unit)

  clickDecrement :: forall g. (Inject Input g) => FreeC g Unit
  clickDecrement = liftFC (inj (ClickDecrement unit) :: g Unit)

  counterComponent :: forall g. (MonadRec g) => Component Number Input g Void
  counterComponent = Component { render : render, query : query }
    where
    eval :: forall g. (Monad g) => Natural Input (StateT Number g)
    eval (ClickIncrement next) = do
      modify (+1)
      return next
    eval (ClickDecrement next) = do
      modify (flip (-) 1)
      return next

    render :: Number -> HTML Void (Input Unit)
    render n = H.text (show n)

    query :: forall g i. (MonadRec g) => FreeC Input i -> StateT Number g i
    query = runFreeCM eval

  test :: FreeC Input Unit
  test = do
    clickIncrement
    clickIncrement
    clickDecrement
@jdegoes

This comment has been minimized.

Copy link
Contributor

jdegoes commented Jun 23, 2015

Is there a problem with using FreeC/Coyoneda? I thought it was previously mentioned in here.

That's fine, the main benefit is it removes the need to write a Functor instance.

Although, if we use Free, the user can always use FreeC, and we can provide helpers to simplify things; i.e. Free is the more fundamental abstraction because FreeC is just a type synonym for it. This way if you have something which is already a functor, you can use it without FreeC.

On the other hand, need to consider how Coproduct will interact with Free and / or FreeC. 😄

@garyb

This comment has been minimized.

Copy link
Member

garyb commented Jun 23, 2015

That's fine, the main benefit is it removes the need to write a Functor instance.

Yeah, that's what I had in mind.

Although, if we use Free, the user can always use FreeC

Oh yeah 😀 I'm just being slow.

@garyb

This comment has been minimized.

Copy link
Member

garyb commented Jun 23, 2015

I really like the look of this, just playing around with it a bit to try and figure out what some of our use cases would look like. I think moving the effect stuff out of the user input is an excellent idea. Of course this castle of handlers and lenses we've currently built will have to be rearranged somewhat to accommodate this change, and we'll perhaps have to reintroduce a bunch of recently eliminated Input types but I think it should certainly improve testability a great deal.

@jdegoes

This comment has been minimized.

Copy link
Contributor

jdegoes commented Jun 23, 2015

I really like the look of this, just playing around with it a bit to try and figure out what some of our use cases would look like.

Sounds good. In particular, I'm interested in eliminating our use of Driver and in allowing direct parent-to-child component communication, among other things. Basically addressing all the negatives listed above in the current definition of component. So any feedback you have on whether or not this proposal is adequate would be quite helpful!

I think moving the effect stuff out of the user input is an excellent idea

Yes, all the effects move to the query processor, which could be a monolithic thing or could be divided into state effects (i.e. "pure" effects) and other effects (g) and then composed.

There are lots of benefits of this approach, for example, parents can query deeply nested child components, all query DSLs are typed, you still have a driver if you need one (e.g. ticking the clock or something). But I don't know what I don't know, so feedback, please!

@cryogenian

This comment has been minimized.

Copy link
Member

cryogenian commented Jun 24, 2015

I like it more then previous component approaches.

@cryogenian

This comment has been minimized.

Copy link
Member

cryogenian commented Jun 24, 2015

I think it's possible to extend this approach https://github.com/slamdata/slamdata/issues/291#issuecomment-114575827
to purescript-halogen.

What if we make following

newtype Blinker f a = Blinker f a 
instance functorBlinker :: (Applicative f) => Functor (Blinker f)
instance applyBlinker :: (Applicative f) => Apply (Blinker f) 
instance applicativeBlinker :: (Applicative f) => Applicative (Blinker f) 
instance bindBlinker :: (Applicative f) => Bind (Blinker f) 
instance monadBlinker :: (Applicative f) => Monad (Blinker f) 
  • Blinker -- something like Behaviour, event driven var
  • f -- underlying monad of signal functions between Blinkers
  • a -- type of event driven value

Blinker is applicative and probably monad. <*> will do subscribing between event dependent function and event dependent value.

data HTML i = Rendered VTree | NotRendered (Spec i)
type VTree = Exists (HTML i)  -- or real vtree from virtual-dom

type Component f i = Blinker f {view :: VTree, model :: i}

componentify :: forall i. HTML i -> i -> Component f i 
componentify html initialState = todo 

-- don't know what is better
componentize :: forall i. Blinker (HTML i) -> i -> Component f i 
componentize html initalState = todo 

-- to be able render list of children
unWrap :: forall i f m. Blinker f (m i) -> m (Blinker f i) 
unWrap  = todo 

wrap :: forall i f m. m (Blinker f i) -> Blinker f (m i) 
wrap = todo
  • HTML i has two cases -- one with typed state, one with hidden state
    • Spec i -- case with typed state can have handlers only with type i -> i
  • Component f i is permanently binded by Blinker view and model. Where model has same type that HTML i from render function.
  • After we made components we can take view and put it to other component Spec i. All view handlers will change model instantly.
module ClickComponent where 
import Halogen.Blinker 

blinker :: forall e. Blinker Identity VTree 
blinker = do 
  counter <- pure 0 
  {model: clickSignal, view: view} <- componentize (render <$> counter) Nothing 
  case clickSignal of 
    Just Increment -> do 
      modify counter (+ 1) 
      modify clickSignal (const Nothing) 
    Just Decrement -> do 
      modify counter (flip (-) 1)
      modify clickSignal (const Nothing)
    Nothing -> pure unit
  pure view 

data I = Increment | Decrement
render :: Number -> Spec (Maybe I) 
render num = div [ text (show num), button [onClick (const Increment)] [], button [onClick (const Decrement)]

test :: forall e. Blinker Identity VTree
test = 
  let trees = traverse (flip componentize Nothing) (((render <$>) <$>) unWrap (pure [1, 2, 3]))
  pure $ div trees

main = 
  node <- getElementById "container" 
  runBlinker node $ blinker
  node' <- getElementById "container-lst" 
  runBlinker node' $ test

What do you think?

@garyb

This comment has been minimized.

Copy link
Member

garyb commented Jun 24, 2015

I'm not sure I see how that solves the issued outlined in the opening issue, or how it would support placeholders so you could install components inside each other. Maybe it can, I just don't quite follow what you're trying to show here.

I forgot to mention, last night I updated the Coyoneda version of ClickComponent to keep query :: forall i. Free f i -> StateT s g i: https://github.com/slamdata/purescript-halogen/blob/ea8e403a5508e39ac5afd873600528973e3b2156/src/HalogenC.purs I don't think we'll have any issues with Coproduct / Free / FreeC interactions, as the Coyoneda moves into the Inject constraint so that stays Free anyway.

@cryogenian

This comment has been minimized.

Copy link
Member

cryogenian commented Jun 24, 2015

It has no component, it has only event dependent variables. Component is aggregation of two event dependent values: VTree and i

  • Queries. You don't need one. It's possible to extract all data in Blinker at any time
  • Localization. You can produce event dependent subgraph, as a record or any other type, once it will be putted to runBlinker it starts to propagate data between nodes.
  • Invariance. There is no such problem because rendered Component has VTree that can be putted to HTML i with any i. And we are not lose any type information in model, because model :: i

There is no special mechanic to install placeholders, because there is no placeholders. One can use

install :: forall i. VTree -> HTML i
install placeholder = div [placeholder]

As an analogue of postRender one can use just propagated VTree (or leave it as it is)

foo = do 
  {view: view, m: m} <- componentize dummyHtml 0  
  modify viewModification view 

Foreign components can be made with some custom function not componentize that provide both VTree and i.

I'm not sure but probably it worth to introduce an event type for this system (It will have Behaviour and Event then and be completely congruent with other frp) that will be exactly Blinker but without state.

newtype Blink f a = forall f. (Applicative f) => Blink f a 
instance plusBlink :: Plus Blink 
accumulate :: forall s a f. (s -> a -> s) -> s ->  Blink f a -> Blinker f s 
componentize :: forall f i. Blink f (HTML i) -> {events :: Blink f i, view :: Blinker f VTree}

I believe that main problems of composition are typed views and mixing concepts of rendered html and html-dsl. DSL must be typed, but rendered html must not. Decoupling current HTML i to Event i and VTree can solve this problem.

@garyb

This comment has been minimized.

Copy link
Member

garyb commented Jun 24, 2015

Queries. You don't need one. It's possible to extract all data in Blinker at any time

I think the problem with that is it doesn't work for foreign components - we might want to read something from a component which is not available in the state we store ourselves (say, the position of the cursor in an Ace editor or something like that).

@cryogenian

This comment has been minimized.

Copy link
Member

cryogenian commented Jun 24, 2015

Push

aceCursorBlinker :: forall e. AceEditor -> Blink (Eff e) Cursor 
aceCursorBlinker editor = 
  wrap $ onCursorChange editor $ trigger $ getCursor editor

Pull

newtype Pull f a = Pull f a 
aceCursorPuller :: forall e. AceEditor -> Pull (Eff e) Cursor 
aceCursorPuller editor = 
  wrap $ pure $ getCursor editor 

Not so general, but I believe that there is the way to do it.

@jdegoes

This comment has been minimized.

Copy link
Contributor

jdegoes commented Jun 24, 2015

I personally think FRP is distinct from Halogen. I think Halogen could be migrated into more an FRP approach, but I'm not sure I want that (since you can use things like purescript-signal, purescript-frp, purescript-rx, etc. in combination with halogen's HTML). Halogen is basically a "state machine" approach to UI, similar to react (but pure, of course), which is a landscape worth exploring (IMO).

With the Component proposal above, we something that is very similar to current Halogen (in spirit), but with a variety of other enhancements, such as localization, queries, non-invariance (but preserving deep queryability), exposing state (which can be serialized, etc.), a driver to query the whole app from the top-level, etc. But open to more feedback (@garyb? Anyone else?).

@paf31

This comment has been minimized.

Copy link
Contributor

paf31 commented Jun 24, 2015

I've been very careful to avoid using the term "FRP" when talking about Halogen in public, since it's definitely got nothing to do with continuous time and there's no denotation (taking Conal Elliott's definition). I had a go at trying to write down a denotation at BayHac, but the obvious stream transformer interpretation wouldn't quite go through. You run into trouble because you either work with SF (and then you don't have a value at time zero) or with SF1 (and lose Category, even though the semantic model is clearly a category). It might be interesting to try to write down a denotation for the new model.

@garyb

This comment has been minimized.

Copy link
Member

garyb commented Jun 24, 2015

I think I agree with @jdegoes here. Although it may be simpler in concept, if we did move to a lower level more-FRP based approach we'd probably end up inventing a lot of machinery to support our common use cases anyway, as in a larger project that's pretty inevitable to keep things sane.

@garyb

This comment has been minimized.

Copy link
Member

garyb commented Jun 24, 2015

Also I'm quite a fan of the current Halogen, so if we can keep something similar but with these fixes in place I'll be very happy. It's by far the least painful UI framework type thing I've ever dealt with.

@cryogenian

This comment has been minimized.

Copy link
Member

cryogenian commented Jun 24, 2015

I do like current Halogen too. It works and does everything we need (but with some hacks obviously 😄 ) Seriously, I don't after those Free, query and install, I like it. I just try to offer other way to think about composition of ui in purescript.

@jdegoes Blinker is state machine approach too. In fact it is something like state machine or EffRef or SF1.
@garyb Now we have only one component. To switch to multicomponent approach we definitely need to discover patterns, write snippets, solve some unexpected problems.

@jdegoes

This comment has been minimized.

Copy link
Contributor

jdegoes commented Jun 25, 2015

It might be interesting to try to write down a denotation for the new model.

I agree. I think render and query might have denotations, I am not sure Component does. Modularity in a UI requires an abstraction which bundles the MVC (or equivalent), and any attempt to do that results in a circular flow of information that seems to defy denotation and complicate composition.

Albeit, maybe it doesn't matter so much, since for the majority of UI code, you can compose views, query processors, etc., only in a few places are you forced into a situation where you need to raise to the level of a component and compose components. So maybe on a practical levels, denotations for HTML, render, query would be sufficient.

@jdegoes

This comment has been minimized.

Copy link
Contributor

jdegoes commented Jun 25, 2015

Any other comments on this component proposal before it becomes scheduled and prioritized for 0.5?

@garyb

This comment has been minimized.

Copy link
Member

garyb commented Jun 25, 2015

I'm going to spend a bit more time experimenting with it tonight, but looks great to me so far. I've done a little work on it now too, reinstated the p type for HTML, written the Functor Component instance, etc.

@jdegoes

This comment has been minimized.

Copy link
Contributor

jdegoes commented Jun 25, 2015

As you experiment, here is all the code I have for my own tests.

@jdegoes

This comment has been minimized.

Copy link
Contributor

jdegoes commented Jun 25, 2015

Btw there is tension between wanting to define MonadState for QueryT, and wanting to define MonadTrans. Because of the order of type parameters, you cannot define both. In the above code, I defined MonadState, and introduced effect to serve the role of lift. Not sure if that's the best decision, but the surface area you get for MonadState seems much larger than what you get for MonadTrans.

@garyb

This comment has been minimized.

Copy link
Member

garyb commented Jun 25, 2015

Ah okay, I did wonder about that.

@jdegoes

This comment has been minimized.

Copy link
Contributor

jdegoes commented Jun 25, 2015

The other two functions that could be added to Component are initialize and finalize. These would have types StateT s g Unit or some such. runUI would call one and the other, while the install functions would call them in response to changes in the installation of child components. Not sure of the applications yet but could eventually lead to re-imagining of widget in favor of something more like web components.

@garyb

This comment has been minimized.

Copy link
Member

garyb commented Jun 25, 2015

re: installAll / installL / installR, can you explain a bit about their uses? Also I'm not quite sure I see how installL or installR could be used to implement installAll.

@jdegoes

This comment has been minimized.

Copy link
Contributor

jdegoes commented Jun 25, 2015

They are used for installing child components into parent components. You define the parent component in QueryT (updating your state, querying children components, etc.), and then at some point, install children into the parent.

You don't so much as install children as specify which "child factory" will be used to create children in response to dynamically generated p. The child factory is a function that, given a p, can provide the initial state of the component together with the component itself.

The installation functions manage the queries and so forth and expose a composite component.

Installation is the most complex case of component composition. If two components do not need to communicate, then simpler helper functions can be defined with correspondingly simpler type signatures.

@jdegoes

This comment has been minimized.

Copy link
Contributor

jdegoes commented Jun 25, 2015

As for the differences between them, I'm not sure I like the current formulation, but the idea is, you might have a sum type of placeholders (represented here explicitly using Either), and may only want to install some of them, leaving the remainder as free / open placeholders.

As for how you can implement installAll using one of the others, well, Component defines a functor in p, so you can map a p to Either p Void and then install the left-hand side, leaving Either p' Void, which you can then map to p' using absurd.

@garyb

This comment has been minimized.

Copy link
Member

garyb commented Jun 25, 2015

Ah okay, yeah it was the second part of that I was unsure about - what the differences between them were.

@jdegoes

This comment has been minimized.

Copy link
Contributor

jdegoes commented Jun 25, 2015

Added two new sections above:

Composition

Child components can be installed into parent components. Parent components can communicate with their children, as well as all descendants. Sibling components can communicate only through the facilitation of a parent component.

This communication architecture ensures that parent components remain in control of information flow and promotes modular architecture.

Asynchronous Behavior

By design, components cannot block rendering, because they are split into pure rendering and possibly effectful query processing parts.

In addition, there is no easy way for race conditions to accidentally overwrite state, because all query processing occurs in a StateT monad, and query processors do not have direct access to state (and therefore cannot easily capture over it). This does not prevent such conditions from occurring, but it does make it much harder for a user to accidentally squash state.

@garyb

This comment has been minimized.

Copy link
Member

garyb commented Jun 26, 2015

I made a bunch of progress with installAll (I'd already started, so figure I'll reimplement in terms of L or R later), but basically fell at the last hurdle: cda6fcc#diff-9609f1afd35b37030f8c8b2330779269R72

From where it's at there, it looks like we need Comonad f' to go any further, but I'm sure that's not right. Any ideas?

@jdegoes

This comment has been minimized.

Copy link
Contributor

jdegoes commented Jun 26, 2015

Will take a close look at this tomorrow, but that doesn't sound quite right -- f' is input to the child query processor, so you should never need to "get out" of it. When you execute the query, f' i gets transformed by the query processor (which is a natural transformation from f' to g) to StateT s' g i, which can be lifted to the output result type.

@garyb

This comment has been minimized.

Copy link
Member

garyb commented Jun 26, 2015

I see where I'm going wrong actually, using pure to try and get f' i -> Free f' i (it's Free f' (f' i)). Should be using prj instead I think, so I never have f' i in the first place.

@garyb

This comment has been minimized.

Copy link
Member

garyb commented Jun 26, 2015

Oh, no, that's not quite right either. But anyway, I need to get from Free (Coproduct f (ChildF p f')) to Free f' i.

@garyb

This comment has been minimized.

Copy link
Member

garyb commented Jun 26, 2015

Well, here's one way of doing it 3eec1a5.

lowerCoyoneda mapF liftFC q seems a bit odd, but that's the only way I could see to get forall f i. (Functor f) => f i -> Free f i

@jdegoes

This comment has been minimized.

Copy link
Contributor

jdegoes commented Jun 26, 2015

@garyb While I review your code this morning, something for you to think about: How about generalizing Component a bit, i.e.:

newtype Component s f g p = Component 
  { render  :: s -> HTML p (f Unit),
    query   :: forall i. f i -> StateT s g i }

The only possible type safe implementation of the query method in PureScript will involve Free or transformers thereof (Coproduct, etc.), but there's no real need to specify it in the definition of Component unless we have to.

@garyb

This comment has been minimized.

Copy link
Member

garyb commented Jun 26, 2015

Wouldn't that cause problems for render though, as the result would end up as HTML p (Free f Unit)?

@jdegoes

This comment has been minimized.

Copy link
Contributor

jdegoes commented Jun 26, 2015

Wouldn't that cause problems for render though, as the result would end up as HTML p (Free f Unit)?

Ah, yes. The implications of this are that the view is no longer limited to a single command to the query processor. Rather, it can spawn a whole "tree" of commands. I'm not sure if that's good or bad, but it's definitely more powerful and something we need to think about carefully — if we want the view to have that kind of power.

@jdegoes

This comment has been minimized.

Copy link
Contributor

jdegoes commented Jun 26, 2015

if we want the view to have that kind of power.

May be worth sketching it out and see if it leads to any great simplification. @puffnfresh was previously in favor of this change (generalizing from Free f to just f), although we didn't talk about the above implications for the view.

@garyb

This comment has been minimized.

Copy link
Member

garyb commented Jun 29, 2015

Should we render after every query? I guess we have to, as there's not really a way of knowing if query did update the state, and even if it did, then we don't know if it updated state relating to the UI.

@jdegoes

This comment has been minimized.

Copy link
Contributor

jdegoes commented Jun 29, 2015

Should we render after every query? I guess we have to, as there's not really a way of knowing if query did update the state, and even if it did, then we don't know if it updated state relating to the UI.

For now, yes. However, it's easy to make install combinators smart by requiring an Eq constraint, such that cache the rendered HTML of their children and only re-render if their state changes. Then if runUI uses the same trick, you have bottom-to-top caching of the entire UI hierarchy.

@garyb

This comment has been minimized.

Copy link
Member

garyb commented Jul 8, 2015

Where we're at currently I'm not really sure how we can render after every query actually - wouldn't we need to change query's type to something like this?

query :: forall i. f i -> StateT s g (Tuple (HTML p (f Unit)) i)
@jdegoes

This comment has been minimized.

Copy link
Contributor

jdegoes commented Jul 8, 2015

Internal queries can only occur in response to events. Assuming the event machinery calls the driver, and the driver is the sole means of feeding events to the app component, then it's sufficient to re-render after every time the driver is invoked. I'm not sure if that's the best way of solving the problem, but it should work, right?

@garyb

This comment has been minimized.

Copy link
Member

garyb commented Jul 8, 2015

Oh, of course - sorry. I was thinking about external querying, but I don't think that works anyway.

@garyb garyb referenced this issue Jul 15, 2015

Closed

New components #147

@garyb garyb referenced this issue Aug 13, 2015

Merged

New components #152

@jdegoes jdegoes closed this in #152 Aug 13, 2015

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment