{"action":"created","issue":{"url":"https://api.github.com/repos/slamdata/purescript-halogen/issues/138","labels_url":"https://api.github.com/repos/slamdata/purescript-halogen/issues/138/labels{/name}","comments_url":"https://api.github.com/repos/slamdata/purescript-halogen/issues/138/comments","events_url":"https://api.github.com/repos/slamdata/purescript-halogen/issues/138/events","html_url":"https://github.com/slamdata/purescript-halogen/issues/138","id":86733039,"number":138,"title":"New Halogen Components","user":{"login":"jdegoes","id":156745,"avatar_url":"https://avatars.githubusercontent.com/u/156745?v=3","gravatar_id":"","url":"https://api.github.com/users/jdegoes","html_url":"https://github.com/jdegoes","followers_url":"https://api.github.com/users/jdegoes/followers","following_url":"https://api.github.com/users/jdegoes/following{/other_user}","gists_url":"https://api.github.com/users/jdegoes/gists{/gist_id}","starred_url":"https://api.github.com/users/jdegoes/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jdegoes/subscriptions","organizations_url":"https://api.github.com/users/jdegoes/orgs","repos_url":"https://api.github.com/users/jdegoes/repos","events_url":"https://api.github.com/users/jdegoes/events{/privacy}","received_events_url":"https://api.github.com/users/jdegoes/received_events","type":"User","site_admin":false},"labels":[{"url":"https://api.github.com/repos/slamdata/purescript-halogen/labels/enhancement","name":"enhancement","color":"84b6eb"},{"url":"https://api.github.com/repos/slamdata/purescript-halogen/labels/idea","name":"idea","color":"fad8c7"}],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":13,"created_at":"2015-06-09T20:58:12Z","updated_at":"2015-06-24T14:31:18Z","closed_at":null,"body":"# Introduction\r\n\r\nHalogen components currently suffer from a variety of drawbacks:\r\n\r\n * **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. \r\n * **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.\r\n * **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.\r\n\r\nTo 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.\r\n\r\nThis 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.\r\n\r\n## Component\r\n\r\n```purescript\r\nnewtype Component s f g p = Component \r\n  { render  :: s -> HTML p (f Unit),\r\n    query   :: forall i. Free f i -> StateT s g i }\r\n    \r\ninstance functorComponent :: Functor (Component s f g)\r\n```\r\n\r\nA component is a bundle of two functions:\r\n\r\n1. `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.\r\n2. `query` — This is a function to query the component, which may or may not modify the state or have other effects up to `g`.\r\n\r\nThe type parameters have the following meanings:\r\n\r\n * `s` - The type of the component state. A component is invariant in `s`.\r\n * `f` - The component's query algebra, whose interpretation has effects up to `StateT s g`.\r\n * `g` - The monad describing the component's extra (non-`State`) effects.\r\n * `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.\r\n\r\n\r\n### Rendering\r\n\r\nA 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. \r\n\r\nBy 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.\r\n\r\nUser 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.\r\n\r\n### Querying\r\n\r\nA 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.\r\n\r\nThe 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`.\r\n\r\n## Pure Component\r\n\r\nPure components are simply components whose effects are limited to component state.\r\n\r\n```purescript\r\ntype ComponentPure s f p = forall g. (Monad g) => Component s f g p\r\n```\r\n\r\n## Classic Halogen Components\r\n\r\nClassic Halogen components are simply a special case of normal components, where all inputs in the query DSL are commands that return `Unit`.\r\n\r\n```purescript\r\nmodule ClickComponent where\r\n  import Control.Monad.State.Trans\r\n  import Control.Monad.State.Class(modify)\r\n  import Control.Monad.Free\r\n  import Control.Monad.Rec.Class\r\n\r\n  import Data.Inject\r\n  import Data.Void\r\n  import Data.Identity\r\n\r\n  import Halogen  \r\n  \r\n  data Input a = ClickIncrement a | ClickDecrement a\r\n\r\n  instance functorMyComponent :: Functor Input where\r\n    (<$>) f (ClickIncrement a) = ClickIncrement (f a)\r\n    (<$>) f (ClickDecrement a) = ClickDecrement (f a)\r\n\r\n  clickIncrement :: forall g. (Functor g, Inject Input g) => Free g Unit\r\n  clickIncrement = liftF (inj (ClickIncrement unit) :: g Unit)\r\n\r\n  clickDecrement :: forall g. (Functor g, Inject Input g) => Free g Unit\r\n  clickDecrement = liftF (inj (ClickDecrement unit) :: g Unit)\r\n\r\n  counterComponent :: forall g. (MonadRec g) => Component Number Input g Void\r\n  counterComponent = Component { render : render, query : query }\r\n    where\r\n      eval :: forall g a. (Monad g) => Input (Free Input a) -> StateT Number g (Free Input a)\r\n      eval (ClickIncrement next) = do\r\n        modify (+1)\r\n        return next\r\n      eval (ClickDecrement next) = do\r\n        modify (flip (-) 1)\r\n        return next\r\n      \r\n      render :: Number -> HTML Void (Input Unit)\r\n      render n = todo\r\n      \r\n      query :: forall g i. (MonadRec g) => Free Input i -> StateT Number g i\r\n      query = runFreeM eval\r\n\r\n  test :: Free Input Unit\r\n  test = do\r\n    clickIncrement\r\n    clickIncrement\r\n    clickDecrement\r\n```\r\n\r\nBy 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).\r\n\r\n## Foreign Components\r\n\r\n```purescript\r\nmodule EditorComponent where\r\n  import Control.Monad.State.Trans\r\n  import Control.Monad.State.Class(modify)\r\n  import Control.Monad.Free\r\n  import Control.Monad.Rec.Class\r\n  import Control.Monad.Eff\r\n  import Control.Monad.Trans(lift)\r\n  import Control.Apply((*>))\r\n\r\n  import Data.Inject\r\n  import Data.Void\r\n  import Data.Identity\r\n\r\n  import Halogen  \r\n\r\n  data Input a = GetContent (String -> a) | SetContent a String | GetCursor (Number -> a)\r\n\r\n  instance functorInput :: Functor Input where\r\n    (<$>) = todo\r\n  \r\n  getContent :: forall g. (Functor g, Inject Input g) => Free g String\r\n  getContent = liftF (inj (GetContent id) :: g String)\r\n  \r\n  setContent :: forall g. (Functor g, Inject Input g) => String -> Free g Unit\r\n  setContent s = liftF (inj (SetContent unit s) :: g Unit)\r\n\r\n  getCursor :: forall g. (Functor g, Inject Input g) => Free g Number\r\n  getCursor = liftF (inj (GetCursor id) :: g Number)\r\n  \r\n  editorComponent :: forall eff. Component Unit Input (Eff (dom :: DOM | eff)) Void\r\n  editorComponent = Component { render : render, query : query }\r\n    where \r\n      eval :: forall eff a. Input (Free Input a) -> StateT Unit (Eff (dom :: DOM | eff)) (Free Input a)\r\n      eval (GetContent f  ) = lift $       f <$> effectfulGetContent\r\n      eval (SetContent n s) = lift $ const n <$> effectfulSetContent s\r\n      eval (GetCursor  f  ) = lift $       f <$> effectfulGetCursor\r\n      \r\n      render s = todo\r\n      \r\n      query :: forall eff i. Free Input i -> StateT Unit (Eff (dom :: DOM | eff)) i\r\n      query = runFreeM eval\r\n\r\n  test :: Free Input Unit\r\n  test = do\r\n    cursor  <- getCursor\r\n    content <- getContent\r\n    setContent $ content ++ (show cursor)\r\n\r\n  foreign import data DOM :: !\r\n  \r\n  foreign import effectfulGetContent :: forall eff. Eff (dom :: DOM | eff) String\r\n  foreign import effectfulSetContent :: forall eff. String -> Eff (dom :: DOM | eff) Unit\r\n  foreign import effectfulGetCursor  :: forall eff. Eff (dom :: DOM | eff) Number\r\n```\r\n\r\n## Composing Components\r\n\r\nThe `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.\r\n\r\n```purescript\r\n  installR :: forall s f g pl pr s' f' p'. (Ord pr, Plus g) =>\r\n    Component s f (QueryT s' f' pr p' g s) (Either pl pr) ->   -- parent\r\n    (pr -> Tuple s' (Component s' f' g p'))               ->   -- factory\r\n    Component (InstalledState s s' f' g pl p') (Coproduct f (ChildF pr f')) g (Either pl p')\r\n  installR a f = todo\r\n\r\n  installL :: forall s f g pl pr s' f' p'. (Ord pl, Plus g) =>\r\n    Component s f (QueryT s' f' pl p' g s) (Either pl pr) ->   -- parent\r\n    (pl -> Tuple s' (Component s' f' g p'))               ->   -- factory\r\n    Component (InstalledState s s' f' g pr p') (Coproduct f (ChildF pl f')) g (Either pr p')\r\n  installL a f = todo\r\n\r\n  installAll :: forall s f g p s' f' p'. (Ord pr, Plus g) =>\r\n    Component s f (QueryT s' f' p p' g s) p               ->   -- parent\r\n    (p -> Tuple s' (Component s' f' g p'))                ->   -- factory\r\n    Component (InstalledState s s' f' g p p') (Coproduct f (ChildF p f')) g p'\r\n  installAll a f = todo\r\n\r\n  type ComponentState s f g p = Tuple s (Component s f g p)\r\n\r\n  data ChildF p f i = ChildF p (f i)\r\n\r\n  type ComponentState s f g p = Tuple s (Component s f g p)\r\n\r\n  type InstalledState s s' f' g p p' = \r\n    { parent   :: s, \r\n      children :: Map.Map p (ComponentState s' f' g p'), \r\n      factory  :: p -> ComponentState s' f' g p' }\r\n\r\n  data QueryT s' f' p p' g s a = QueryT (StateT (InstalledState s s' f' g p p') g a)\r\n```\r\n\r\nThe 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.\r\n\r\nThe 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.\r\n\r\n```purescript\r\n  -- queries a particular child from the parent:\r\n  query :: forall s s' f' p p' g. p -> (forall i. Free f' i -> QueryT s' f' p p' g s (Maybe i))\r\n  query p q = todo\r\n\r\n  -- lifts an effect into the QueryT monad:\r\n  effect :: forall s' f' p p' g s a. (Monad g) => g a -> QueryT s' f' p p' g s a\r\n  effect ga = todo\r\n\r\n  -- MonadState for QueryT so parents can manipulate their own state\r\n  instance monadStateQueryT :: MonadState s (QueryT s' f' p p' g s) where\r\n    state f = todo\r\n\r\n  instance functorQueryT :: Functor (QueryT s' f' p p' g s) where\r\n    (<$>) f fa = todo\r\n\r\n  instance applyQueryT :: Apply (QueryT s' f' p p' g s) where\r\n    (<*>) f fa = todo\r\n\r\n  instance applicativeQueryT :: Applicative (QueryT s' f' p p' g s) where\r\n    pure a = todo\r\n\r\n  instance bindQueryT :: Bind (QueryT s' f' p p' g s) where\r\n    (>>=) fa f = todo\r\n\r\n  instance monadQueryT :: Monad (QueryT s' f' p p' g s)\r\n\r\n  instance functorChildF :: (Functor f) => Functor (ChildF p f) where\r\n    (<$>) f (ChildF p fi) = ChildF p (f <$> fi)\r\n```\r\n\r\nTwo functions `query` and `effect`, as well as standard instances for `QueryT`, may be used to easily build parent component query processors:\r\n\r\n```purescript\r\n  test = do \r\n    state <- get\r\n    effect $ doEffect\r\n    set state\r\n    resp <- query editor getContent\r\n    return $ fromMaybe \"\" resp\r\n```\r\n\r\n## Running Components\r\n\r\nTo run a component, you provide the component and an initial state.\r\n\r\nThe component has an arbitrary input DSL `f`, and runs effects in `Aff`. By design, components cannot ever block rendering.\r\n\r\n```purescript\r\n  type Driver f eff = forall i. f i -> Aff (HalogenEffects eff) i\r\n\r\n  runComponent :: forall eff s f. (Functor f) =>\r\n    Component s f (Aff (HalogenEffects eff)) Void ->\r\n    s                                             ->\r\n    Aff (HalogenEffects eff) (Tuple HTMLElement (Driver f eff))\r\n  runComponent c s = todo\r\n```"},"comment":{"url":"https://api.github.com/repos/slamdata/purescript-halogen/issues/comments/114888554","html_url":"https://github.com/slamdata/purescript-halogen/issues/138#issuecomment-114888554","issue_url":"https://api.github.com/repos/slamdata/purescript-halogen/issues/138","id":114888554,"user":{"login":"garyb","id":693642,"avatar_url":"https://avatars.githubusercontent.com/u/693642?v=3","gravatar_id":"","url":"https://api.github.com/users/garyb","html_url":"https://github.com/garyb","followers_url":"https://api.github.com/users/garyb/followers","following_url":"https://api.github.com/users/garyb/following{/other_user}","gists_url":"https://api.github.com/users/garyb/gists{/gist_id}","starred_url":"https://api.github.com/users/garyb/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/garyb/subscriptions","organizations_url":"https://api.github.com/users/garyb/orgs","repos_url":"https://api.github.com/users/garyb/repos","events_url":"https://api.github.com/users/garyb/events{/privacy}","received_events_url":"https://api.github.com/users/garyb/received_events","type":"User","site_admin":false},"created_at":"2015-06-24T14:31:18Z","updated_at":"2015-06-24T14:31:18Z","body":"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.\r\n\r\nI 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."}}