Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign upNew Halogen Components #138
Comments
This comment has been minimized.
This comment has been minimized.
|
What I think: newtype Component f s p q r = Component
{ render :: s -> HTML p q,
query :: q -> StateT s f r } |
This comment has been minimized.
This comment has been minimized.
|
We discussed, problem with |
jdegoes
changed the title
Further development of component ideas
New Halogen Components
Jun 15, 2015
This comment has been minimized.
This comment has been minimized.
|
@puffnfresh Updated. |
jdegoes
added
idea
enhancement
labels
Jun 15, 2015
This comment has been minimized.
This comment has been minimized.
|
What's the status with this, just out of interest? |
This comment has been minimized.
This comment has been minimized.
|
@garyb It's all ready for review, which is going to fall to you and @cryogenian now. This proposal would change 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 |
This comment has been minimized.
This comment has been minimized.
|
Is there a problem with using 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 |
This comment has been minimized.
This comment has been minimized.
That's fine, the main benefit is it removes the need to write a Although, if we use On the other hand, need to consider how |
This comment has been minimized.
This comment has been minimized.
Yeah, that's what I had in mind.
Oh yeah |
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
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!
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 ( 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! |
This comment has been minimized.
This comment has been minimized.
|
I like it more then previous component approaches. |
This comment has been minimized.
This comment has been minimized.
|
I think it's possible to extend this approach https://github.com/slamdata/slamdata/issues/291#issuecomment-114575827 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)
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
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' $ testWhat do you think? |
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
|
It has no component, it has only event dependent variables. Component is aggregation of two event dependent values:
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 foo = do
{view: view, m: m} <- componentize dummyHtml 0
modify viewModification view Foreign components can be made with some custom function not I'm not sure but probably it worth to introduce an event type for this system (It will have 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 |
This comment has been minimized.
This comment has been minimized.
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). |
This comment has been minimized.
This comment has been minimized.
|
Push aceCursorBlinker :: forall e. AceEditor -> Blink (Eff e) Cursor
aceCursorBlinker editor =
wrap $ onCursorChange editor $ trigger $ getCursor editorPull 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. |
This comment has been minimized.
This comment has been minimized.
|
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?). |
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
I do like current Halogen too. It works and does everything we need (but with some hacks obviously @jdegoes |
This comment has been minimized.
This comment has been minimized.
I agree. I think 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. |
This comment has been minimized.
This comment has been minimized.
|
Any other comments on this component proposal before it becomes scheduled and prioritized for 0.5? |
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
|
As you experiment, here is all the code I have for my own tests. |
This comment has been minimized.
This comment has been minimized.
|
Btw there is tension between wanting to define |
This comment has been minimized.
This comment has been minimized.
|
Ah okay, I did wonder about that. |
This comment has been minimized.
This comment has been minimized.
|
The other two functions that could be added to Component are |
This comment has been minimized.
This comment has been minimized.
|
re: |
This comment has been minimized.
This comment has been minimized.
|
They are used for installing child components into parent components. You define the parent component in You don't so much as install children as specify which "child factory" will be used to create children in response to dynamically generated 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. |
This comment has been minimized.
This comment has been minimized.
|
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 As for how you can implement |
This comment has been minimized.
This comment has been minimized.
|
Ah okay, yeah it was the second part of that I was unsure about - what the differences between them were. |
This comment has been minimized.
This comment has been minimized.
|
Added two new sections above: CompositionChild 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 BehaviorBy 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 |
This comment has been minimized.
This comment has been minimized.
|
I made a bunch of progress with From where it's at there, it looks like we need |
This comment has been minimized.
This comment has been minimized.
|
Will take a close look at this tomorrow, but that doesn't sound quite right -- |
This comment has been minimized.
This comment has been minimized.
|
I see where I'm going wrong actually, using |
This comment has been minimized.
This comment has been minimized.
|
Oh, no, that's not quite right either. But anyway, I need to get from |
This comment has been minimized.
This comment has been minimized.
|
Well, here's one way of doing it 3eec1a5.
|
This comment has been minimized.
This comment has been minimized.
|
@garyb While I review your code this morning, something for you to think about: How about generalizing
The only possible type safe implementation of the |
This comment has been minimized.
This comment has been minimized.
|
Wouldn't that cause problems for |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
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 |
jdegoes
referenced this issue
Jun 28, 2015
Closed
Provide a way to maintain separate state for components #137
garyb
self-assigned this
Jun 28, 2015
jdegoes
referenced this issue
Jun 28, 2015
Closed
Provide way to give Halogen's state to a component #136
This comment has been minimized.
This comment has been minimized.
|
Should we |
This comment has been minimized.
This comment has been minimized.
For now, yes. However, it's easy to make |
This was referenced Jun 30, 2015
garyb
referenced this issue
Jun 30, 2015
Closed
Attributes v. Properties, looking toward SVG support #133
This comment has been minimized.
This comment has been minimized.
|
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 :: forall i. f i -> StateT s g (Tuple (HTML p (f Unit)) i) |
This comment has been minimized.
This comment has been minimized.
|
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? |
This comment has been minimized.
This comment has been minimized.
|
Oh, of course - sorry. I was thinking about external querying, but I don't think that works anyway. |
jdegoes commentedJun 9, 2015
Introduction
Halogen components currently suffer from a variety of drawbacks:
To work around the first two issues, the emerging anti-pattern involves passing around
Driverso 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
A component is a bundle of two functions:
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.query— This is a function to query the component, which may or may not modify the state or have other effects up tog.The type parameters have the following meanings:
s- The type of the component state. A component is invariant ins.f- The component's query algebra, whose interpretation has effects up toStateT 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 aFunctorinp, 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 theHTMLrendering must neither know nor care what the query processor does with them. Rather, theHTMLrendering 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
HTMLrendering to interact with the query processor.Querying
A component also exposes a query DSL, which is a
Freealgebra inf. Classic Halogen components consist purely of "write-only" commands, which returnUnit, 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 usingStateT. Thus, the query processor may implement the query DSL using both the component state (reads and writes), as well as additional effects captured byg.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
StateTmonad, 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.
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.By design, the commands that can be sent to the component (
clickIncrement, andclickDecrement) work in anyFreealgebra which containsInput(including, of course,Inputitself).Foreign Components
Composing Components
The
installfamily 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.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
QueryTfor some base monadg. This monad transformer allows the parent to manipulate its own state, and send and received typed messages to its children. After installation usinginstallR,installLorinstallAll, 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.Two functions
queryandeffect, as well as standard instances forQueryT, may be used to easily build parent component query processors: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 inAff. By design, components cannot ever block rendering.