Coder Social home page Coder Social logo

Comments (18)

natefaubion avatar natefaubion commented on July 22, 2024

Basically something like this, if this makes sense

data Respond a b = Respond a | Continue b
listen :: forall a b r p f. (Functor f) => (a -> r) -> (b -> r) -> HTML p (f (Respond a b)) -> HTML p (f r)

Which essentially looks like mapping either or something. I don't really like using Either everywhere though for something like this since we already use it for the request handler, but that's just me.

from purescript-halogen.

natefaubion avatar natefaubion commented on July 22, 2024

Here's what I landed on

module App where

import Data.Tuple
import Data.Maybe
import Control.Monad.Eff

import Halogen
import Halogen.Signal
import qualified Halogen.HTML as H
import qualified Halogen.HTML.Attributes as A
import qualified Halogen.HTML.Events as A
import qualified Halogen.HTML.Events.Forms as A
import qualified Halogen.HTML.Events.Handler as E

data Respond a b = Respond (Maybe a) b

listen :: forall a b r. (Maybe a -> b -> r) -> Respond a b -> r
listen f (Respond a b) = f a b

---

type CountState = Number
data CountEvent = Counted
data CountInput = Count

countTil :: Number -> CountState -> H.HTML _ (Respond CountEvent CountInput)
countTil til n =
  H.div_
    [ H.button (A.onclick \_ -> pure $ handleClick (n + 1)) [ H.text "count" ]
    , H.text (show n) ]
  where
  handleClick n | n == til  = Respond (Just Counted) Count
  handleClick n | otherwise = Respond Nothing        Count

countUpdate :: CountState -> CountInput -> CountState
countUpdate n _ = n + 1

---

type State =
  { counted :: Boolean
  , counter :: CountState }

data Input = CInput (Maybe CountEvent) CountInput

view :: PureView Input
view = render <$> stateful { counted: false, counter: 0 } update
  where
  render state =
    H.div_
      [ listen CInput <$> countTil 5 state.counter
      , if state.counted
           then H.text "Counted!"
           else H.text "Not counted" ]

  update state (CInput e c) = case e of
    Just Counted -> state { counter = countUpdate state.counter c, counted = true }
    _            -> state { counter = countUpdate state.counter c }

from purescript-halogen.

jdegoes avatar jdegoes commented on July 22, 2024

This looks like a good way of passing information from child to parent. It's a bit bulky but gets the job done.

from purescript-halogen.

natefaubion avatar natefaubion commented on July 22, 2024

Any other suggestions would certainly be welcome. The main problem is that you have to always be able to pass along the component state. I had originally started with an Either variant, but that means you could only update the state or bubble an event, not both at the same time. Which means that the example above would stop working. Ideally I would like the interface of listen CEvent CInput <$> component so I wouldn't have to pattern match on state/event at the same time, but that would require you to run the update function twice (once for the component state, and once for the event).

from purescript-halogen.

jdegoes avatar jdegoes commented on July 22, 2024

@natefaubion

We were chatting about something like this:

type UI p m req res = exists i. SF1 (Either i req) (HTML p (m (Either i res)))

The idea is that a UI can both:

  1. Accept "requests" of type req, and produce "responses" of type res;
  2. Generate events from user input and consume them internally without having to do loopback.

It can effectfully produce internal events and respond to requests with some effect type m, almost certainly going to be Aff e or Par e for some set of effects e. Could also be pure or a free monad.

Although totally unproven at this point, this approach simplifies a lot of things:

  1. Eliminates a few abstractions in favor of pushing more power into the View AKA UI.
  2. Allows parents to pass information to children.
  3. Allows children to pass information to parents.
  4. Eliminates loop back at the cost of not being easily able to mock out user events (which is a key benefit of the current approach).

Thoughts @garyb, @natefaubion, @cryogenian, @paf31?

from purescript-halogen.

cryogenian avatar cryogenian commented on July 22, 2024
  • I am afraid I can't see how this approach can make communication simpler. We have req and res in both input and output of SF1.
  • Not sure about i req res. It simplifies request handler, but maybe gets too much power to view.
  • Using monads in View will make it identical with purescript-signal Channel approach 😄 One can in example send driver in message to other component, and send any message in any time from any function.

In general, I think that current approach can work very nice with some MVI/MVC other MVstuff architecture. Maybe it's worth to make version of View that will consume i and produce Html p r not Html p (Either i r) to make MVstuff more clean.

Can typeclasses help to aggregate Views?

from purescript-halogen.

natefaubion avatar natefaubion commented on July 22, 2024

As a thought project, here's how I would use the typical MV* pattern to propagate requests/inputs/events.

-- Event handlers need to be able to propagate user events, new inputs, and
-- effect requests. An Either isn't sufficient since you could potentially
-- want to do all three at the same time. For example, you might want to signal
-- a user event and kick off a request at the same time. Since event bubbling
-- must propagate with the input, you can't do an either/or.
type EventResult = Tuple3

proxy :: forall e i r e' i' r'. (e -> e') -> (i -> i') -> (r -> r') -> EventResult e i r -> EventResult e' i' r'
proxy f g h = uncurry \e i r -> tuple3 (f e) (g i) (h r)

-- Bundles the event and input together so it can be handled in the transition function
listen :: forall e i r i' r'. (e -> i -> i') -> (r -> r') -> EventResult e i r -> EventResult Unit i' r'
listen f g = uncurry \e i r -> tuple3 Unit (f e i) (g r)

-- Drops any events
ignore :: forall e i r i' r'. (i -> i') -> (r -> r') -> EventResult e i r -> EventResult Unit i' r'
ignore f g = uncurry \e i r -> tuple3 Unit (f i) (g r)

---

data ComponentReq   = CR1 | CR2 | CR3 (Component2Req)
data ComponentInput = CI1 | CI2 | CI3 (Component2Input)
data ComponentEvent = CE1 | CE2 | CE3
type ComponentState = { status :: Number, child :: Component2State }

renderComponent :: Number -> ComponentState -> HTML _ (EventResult ComponentEvent ComponentInput ComponentReq)
updateComponent :: ComponentState -> ComponentReq -> ComponentState
handleComponent :: forall eff. (ComponentInput -> Aff eff Unit) -> ParentReq -> Aff eff Unit
initialComponent :: ComponentState

---

data ParentReq   = PR1 | PR2 | PR3 ComponentReq
data ParentInput = PI1 | PI2 | PI3 ComponentEvent ComponentInput -- Event Bubbling
data ParentState = { name :: String, component :: ComponentState }

renderParent :: ParentState -> HTML _ (EventResult Unit ParentInput ParentReq)
renderParent state =
  div_
    [ h3 [text state.name]
    -- Bundles up event and state for handling
    , listen PI3 PR3 <$> renderComponent 42 state.component ]

updateParent :: ParentState -> ParentInput -> ParentState
updateParent state PI1 = ...
updateParent state PI2 = ...
updateParent state (PI3 event compState) =
  -- Must handle events and update child state at the same time
  case event of
    CE1 -> newState "Foo"
    CE2 -> newState "Bar"
    CE3 -> newState "Baz"
  where
  newState = { name: _, updateComponent state.component compState }

handleParent :: forall eff. (ParentInput -> Aff eff Unit) -> ParentReq -> Aff eff Unit
handleParent driver PR1 = ...
handleParent driver PR2 = ...
handleParent driver (PR3 r) =
  -- Need to pick an event since its bundled with the child state. I suppose
  -- We can use a Maybe for the event, or require every component to have some
  -- sort of Nil event.
  handleComponent (driver <<< (PI3 CI3)) r 

initialParent :: ParentState
initialParent =
  { name: ""
  , component: initialComponent }

---

type UI p s i r =
  { render :: s -> HTML p (EventResult Unit i r) -- All events must be handled
  , update :: s -> i -> s
  , handle :: forall eff. (i -> Aff eff Unit) -> r -> Aff eff Unit }

-- runUI can do whatever it needs, the Signal machinery is no longer exposed
-- to the user as everything is manually composed
runUI :: ...

TLDR;

  • Either isn't sufficiently expressive because you need to be able to propagate user events and kick off requests at the same time, so I've switched to Tuple3.
  • This is nice because you can guarantee that all user events are handled somehow (even if explicitly ignored)
  • The SF machinery is completely unnecessary, and everything just works by convention. This can be either seen as a win since its just dumb functions, or as a lose because you are manually nesting and dispatching everything and can't use any abstractions to reduce boilerplate (this is essentially the Elm architecture with more boilerplate).
  • Manually nesting/dispatching is very tedious (though completely safe up to exhaustive checking) and requires various forms of composition.

I don't really know how I feel about it. The second you want to bundle everything together in a SF you have to put effects into it, in which case you might as well just bundle everything up as a Widget that contains its own runUI invocation and pass drivers around (this is what I originally asked @paf31 about doing).

from purescript-halogen.

paf31 avatar paf31 commented on July 22, 2024

Looks interesting.

  • Tuple3 implies that every click/key press/focus/whatever emits all three things, which seems a little odd.
  • Do you mean to use Aff in handle? Eff should be enough when Aff gets something like callcc.

I have thought about whether SF is needed too. It's not, because you can express it using functions and an optional existential, but it would involve more plumbing and make composition harder. That said, I'm all in favor of getting something working and then figuring out the abstractions to layer on top later, even if the something is a giant function.

Something here looks very lens-like.

Finally, one more thing jumps to mind about SF - it should be possible to destructure it as follows, keeping abstraction via the existential it is isomorphic to:

unpackSF :: forall i o r. (forall s. { initial :: s, step :: i -> s -> { output :: o, next :: s } } -> r) -> SF i o -> r

This should allow us to represent a "state machine with hidden state" as SF, but still unpack it to compose the state abstractly.

from purescript-halogen.

natefaubion avatar natefaubion commented on July 22, 2024

My effect signatures maybe wrong, I didn't typecheck anything :)

You don't have to emit all three things you just pick Unit or something, just like now you don't have to have requests, and you map some branch of Either over it. If you create a view that doesn't use them and you choose a different result in the event handler, you'd just need to adapt it wherever you embed it.

from purescript-halogen.

paf31 avatar paf31 commented on July 22, 2024

Multiple input from one input is what I previously used callcc for when using Eff. It would allow these same sort of patterns I think. You could feasibly say things like

  • Emit "set busy to true"
  • Do some work
  • Emit the result
  • Emit "set busy to false"

or provide partial progress updates using several inputs, or whatever. Events could probably be handled similarly.

from purescript-halogen.

jdegoes avatar jdegoes commented on July 22, 2024

I am afraid I can't see how this approach can make communication simpler. We have req and res in both input and output of SF1.

That's actually not true. In the existing definition of View, the purpose of i is conflated: i is used by a view to update its own internal state, via loopback (that is, the i's it generates from input events are fed back to itself so it has a chance to update state and the view). Also, you can use i to send information from a parent to the child. You can use the output type Either i r in the existing definition to send information from child to parent.

The conflation of these two inhibits composition because of the need for loopback. It also adds benign boilerplate in the form of having a single sum type include both input events and messages between parent and child.

As a thought project, here's how I would use the typical MV* pattern to propagate requests/inputs/events.

This is a variation of the React theme (which itself is a variation of MVC for some definition of C), although I'd simplify your definition of UI to something like:

type UI eff p s i r =
  { render :: s -> HTML p (EventResult Unit i r) -- All events must be handled
  , update :: s -> i -> s
  , handle :: r -> Aff eff i

Again, the i channel is overloaded for user input events and communication. Among other things, this makes the UI invariant in i (as it appears as a function parameter and return value), which inhibits composition in all the usual ways. It's possible to existentially hide s, but r is yet another problem as its invariant, mainly because the view itself does not have the power to execute effects directly.

Let me run through my proposed signature one more time:

type UI p m req res = exists i. SF1 (Either i req) (HTML p (m (Either i res)))
  1. The user input events are existentially hidden, which means they are not visible in the type signature of UI. As a result, they do not inhibit composition. Only the UI itself knows about the user input events it processes and handles to update its own state.
  2. The UI can accept information from a parent (req), and also push information to a parent (res) in response to some input or a request. UIs that do not perform any communication can just use Void for these parameters. I believe, but am not certain, that UI is a Profunctor in req / res, and therefore has rich composition properties.
  3. The UI is allowed to execute effects, either to process user events into a form where they can be incorporated into the state, or to create some information to communicate to a parent. However, the effects can be limited, e.g. a pure UI is just UI p Identity req res, which can be lifted to Aff eff to compose with an effectful UI. Or effects can be described using a free algebra, and then composed via coproducts, etc. Because the UI can handle its own effects, there's no need to be invariant in another type parameter.
  4. Aside from input / output, the only UI type parameters are p for placeholder and m for effects. Effects combine using the more powerful of the effect, while placeholders combine using Either (and their renderers also combine in such a nice fashion). So really, p and m do not inhibit composition, and req and res permit very rich compositional properties, as well as typed ways of describing UIs that do not accept requests or which do not produce information in response to requests.

The ability to return many res, in this formulation or any other one, does seem to be important; I will think about whether adding something like callcc is the best way to do that or not.

unpackSF :: forall i o r. (forall s. { initial :: s, step :: i -> s -> { output :: o, next :: s } } -> r) -> SF i o -> r

I'm interested in explicit (existential) state. It can improve performance because the exact same function is used for updating the view (rather than creation of more closures which capture more state). You can constraint the state, for example, requiring it be JSON-serializable, if you want, which will lead to other interesting applications. Also if you know the state of a child, because it hasn't been wrapped up yet, then it gives you a way to get information from the child.

Still, with all that said, not exactly sure how it would work.

from purescript-halogen.

paf31 avatar paf31 commented on July 22, 2024

I think we can review and maybe close this in light of merging #50.

from purescript-halogen.

jdegoes avatar jdegoes commented on July 22, 2024

I think this is now fixed, as both parent to child and child to parent communication is now possible, although some use cases will depend on graft as most components will be embedded in other components and the communication story for them is less clear.

from purescript-halogen.

jdegoes avatar jdegoes commented on July 22, 2024

@natefaubion Any comments here, OK closing this one in favor of other issues?

from purescript-halogen.

natefaubion avatar natefaubion commented on July 22, 2024

I don't really get how the machinery in Component solves this issue though, as the issue is mainly about bubbling events in nested components, so maybe that should be brought up as a more specific issue. Can someone maybe provide an example of how req and res are supposed to work to communicate between components?

My immediate thought is that Either is not expressive enough for the example brought up earlier. You'll want to be able to update internal state and signal an event at the same time. If you can only do one or the other you can't build my example.

from purescript-halogen.

natefaubion avatar natefaubion commented on July 22, 2024

In fact I think it would be good to start adapting our examples to use the compositional machinery so we can get a feel for how it works. Right now they are all stand alone toy components.

from purescript-halogen.

jdegoes avatar jdegoes commented on July 22, 2024

@natefaubion Part of this depends on the graft story. A parent component would presumably install a child component into itself, in some placeholder(s), and thereby gain the ability to send to and receive from the child component.

e.g. something like:

install :: forall p p' a a' b b' m node. 
  Component p m node (Either a a') (Either b b') -> 
  Component p' m node b' a' -> 
  Component (Either p p') m node a b
install parent child = ...

parent `install `child

Any evidence that the child existed is gobbled up when the child is installed into the parent, and while the raw parent's types show it can communicate to and from the child, once installed, the types do not contain any evidence of such parent-child communication.

Anyway, there's definitely more work to be done here, but I'd say the issue is more about grafting / combinators than the type signatures, which now permit communication even if there aren't yet the combinators to make that happen.

from purescript-halogen.

natefaubion avatar natefaubion commented on July 22, 2024

Ok, that makes sense.

On Mar 29, 2015, at 9:49 PM, "John A. De Goes" [email protected] wrote:

@natefaubion Part of this depends on the graft story. A parent component would presumably install a child component into itself, in some placeholder(s), and thereby gain the ability to send to and receive from the child component.

e.g. something like:

install :: forall p p' a a' b b' m node.
Component p m node (Either a a') (Either b b') ->
Component p' m node b' a' ->
Component (Either p p') m node a b
install parent child = ...

parent installchild
Any evidence that the child existed is gobbled up when the child is installed into the parent, and while the raw parent's types show it can communicate to and from the child, once installed, the types do not contain any evidence of such parent-child communication.

Anyway, there's definitely more work to be done here, but I'd say the issue is more about grafting / combinators than the type signatures, which now permit communication even if there aren't yet the combinators to make that happen.


Reply to this email directly or view it on GitHub.

from purescript-halogen.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.