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 upProposal: Existentially hidden query algebra #526
Comments
This comment has been minimized.
This comment has been minimized.
|
Here are some more types, and shows how you can recover a backwards compatible spec from this formulation. data RequestBox g a b = RequestBox (g b) (Maybe b -> a)
data HalogenQ (f :: Type) (g :: Type -> Type) (i :: Type) (a :: Type)
= Initialize i
| Receive i
| Handle f
| Request (Exists (RequestBox g a))
| Finalize
data HalogenM f s ps o m a
type ComponentEval f g i s ps o m = HalogenQ f g i ~> HalogenM f s ps o m
type Component' h f g i s ps o m =
{ initialState :: i -> s
, render :: s -> h (ComponentSlot h ps m f) f
, eval :: ComponentEval f g i s ps o m
}
data Component h g i o m
eval
:: forall f g i s ps o m
. { initialize :: i -> HalogenM f s ps o m Unit
, receive :: i -> HalogenM f s ps o m Unit
, handle :: f -> HalogenM f s ps o m Unit
, request :: forall b. g b -> HalogenM f s ps o m (Maybe b)
, finalize :: HalogenM f s ps o m Unit
}
-> ComponentEval f g i s ps o m
eval { initialize, receive, handle, request, finalize } = case _ of
Initialize i -> initialize i
Receive i -> receive i
Handle f -> handle f
Request req -> runExists (\(RequestBox g k) -> k <$> request g) req
Finalize -> finalize
compatEval
:: forall g i s ps o m
. { initializer :: Maybe (g Unit)
, finalizer :: Maybe (g Unit)
, receiver :: i -> Maybe (g Unit)
, eval :: g ~> HalogenM (g Unit) s ps o m
}
-> ComponentEval (g Unit) g i s ps o m
compatEval { initializer, finalizer, receiver, eval } = case _ of
Initialize i -> for_ initializer eval
Receive i -> for_ (receiver i) eval
Handle f -> eval f
Request req -> runExists (\(RequestBox g k) -> k <<< Just <$> request g) req
Finalize -> for_ finalizer eval |
This comment has been minimized.
This comment has been minimized.
|
I've spent a couple of days working on this proposal and actuall come up with other proposal data HalogenQ (f :: Type) (i :: Type)
= Initialize i
| Receive i
| Handle f
| Finalize
data HalogenM f s ps o m a
type ComponentEval f i s ps o m = HalogenQ f i ~> HalogenM f s ps o m
data Component h i o mThe same example from the first comment newtype Toggle = Toggle Boolean
newtype Raise o = Raise o
type State i =
{ visible :: Boolean
, input :: i
}
type ChildSlots o =
( child :: H.Slot o Unit
)
toggleHoc :: forall i o m. Component HH.HTML i o m -> Component HH.HTML i o m
toggleHoc inner = component { initialState, render, eval }
where
initialState :: i -> State i
initialState i =
{ visible: true
, input: i
}
render :: State i -> HH.ComponentHTML (ChildSlots o) m
render state =
HH.div_
[ HH.button
[ HP.onClick (HE.send (Toggle (not state.visible))) ]
[ HH.text "Toggle" ]
, if state.visible
then HH.slot (SProxy :: SProxy "child") unit inner state.input Raise
else HH.text ""
]
eval :: HalogenQ (Raise o) i -> HalogenM (State i) f (ChildSlots o) m Unit
eval = case _ of
Initialize _ -> pure unit
Finalize -> pure unit
Receive input -> H.modify_ _{ input = input }
Handle (Raise o) -> H.raise o This approach for component example https://gist.github.com/cryogenian/ededd916867af50f07b639d28449f3d5 |
This comment has been minimized.
This comment has been minimized.
|
To be honest, I'd remove Alternative to removing algebra at all is data HalogenQ (f :: Type) (r :: Type -> Type) a
= Initialize i
| Request (r a)
| Handle f
| Finalize
data HalogenM f s ps o m a
type ComponentEval f r s ps o m = HalogenQ f r ~> HalogenM f s ps o m
data Component h r o mThe slot changes to something like (not in HOC case in HOC case it remains the same) HH.slot (SProxy :: SProxy "child") unit inner (Const state.input) HandleBut I don't quite sure how to formulate profunctor. BTW, why not make |
This comment has been minimized.
This comment has been minimized.
|
If I understand that first proposal Maxim, you’re saying we should remove the ability to query child components at all? I think we lose something pretty important there, almost the defining feature of what Halogen is. I’m not sure what the second one is saying, is it not the same as Nate’s proposal, but without the internal/external distinction? |
This comment has been minimized.
This comment has been minimized.
|
If we completely remove the query, then we don't even need a natural transformation, since everything is Unit. I'm not proposing we do that, because we lose a big part of Halogen's functionality as @garyb said. One "thing" with my proposal is that it means top-level queries through HalogenIO become partial as well. I admit this is awkward, but I don't see a solution for that. |
This comment has been minimized.
This comment has been minimized.
|
I think your second proposal is the same as mine, except I've added the RequestBox wrapper, which is necessary if we want to support HOCs without using |
This comment has been minimized.
This comment has been minimized.
Not exactly, I propose to remove
Currently components have
We want to add private messages and make public query partial. In that case we would have
Since I agree that it seems that |
This comment has been minimized.
This comment has been minimized.
|
I'm strongly against using inputs for actions, I'd rather lose inputs entirely if we were specifically trying to cut down the communication options between components. Trying to make a continuous stream of things discrete is one of my least favourite things about FRP. It's especially weird in Halogen as it ties rendering to action-sending, you end up having to keep the action-that-we-are-currently-sending in the state of the parent, have the action implement The thing that requests give us is a sensible interface for interacting with 3rd party components. It's impractical to try and keep a copy of the state of a component not under our control in our component's state, so the only way to solve that is to instead use requests to ask for the state instead. There are some cases with just normal HTML that this happens actually, state things that the virtual dom is unaware of like cursor positions in inputs. You could argue that requests aren't strictly necessary, as you could send an input that is supposed to trigger an output with the reply, but that's unpleasant as it's like programming against event-based interfaces. It makes following the intent difficult as code gets split up between request-making and response handling, and can easily leave the component in broken states unless you're careful to ensure that every response is treated as optional. Keep in mind we don't need to expose all these options all the time as part of the component interface - if we add these capabilities to make HOC stuff have less overhead, and allow remapping more easily (that was one of Nate's goals with moving the lifecycle algebra into the component), we can still provide a simplified interface for the normal cases. We could even provide the input-output only version for people who like FRP |
This comment has been minimized.
This comment has been minimized.
|
The point of view where
This is really good point. One problem with initializers and receivers that they're sending actions instead of directly use data HalogenQ f (g :: Type -> Type) a
= Initialize a
| Rerender a
| Handle f a
| Finalize a
| Request (g a) -- I'm not sure if this should be exist'edI.e. we have type ComponentEval s f g ps o m = HalogenQ f g ~> HalogenM s f ps o m
data Component h g o m
type QueryI o
= Toggle Boolean
| Raise o
type State =
{ visible :: Boolean
}
type ChildSlots g o =
( child :: H.Slot g o Unit
)
toggleHoc :: forall g o m. Component HH.HTML g o m -> Component HH.HTML g o m
toggleHoc inner = component { initialState, render, eval }
where
initialState :: State
initialState = { visible: true }
render :: State -> HH.ComponentHTML QueryI (ChildSlots g o) m
render state =
HH.div_
[ HH.button
[ HP.onClick (HE.input_ (Toggle (not state.visible))) ]
[ HH.text "Toggle" ]
, if state.visible
then HH.slot (SProxy :: SProxy "child") unit inner (HE.input Raise)
else HH.text ""
]
eval :: HalogenQ (QueryI o) g ~> HalogenM State f (ChildSlots g o) m
eval = case _ of
Initialize next -> pure next
Finalize next -> pure next
Rerender next -> pure next
Handle (Toggle visible) next -> H.modify _ { visible = visible } $> next
Handle (Raise o) next -> H.raise o $> next
Request q -> H.query (SProxy :: SProxy "child") unit qNote, that since we
Not me definitely, removing algebras and making communication channels only
Does it mean we need this? Request q -> H.query (SProxy :: SProxy "child") unit q *> halt |
This comment has been minimized.
This comment has been minimized.
|
I don't feel like this example illustrates what
Why? How else do you keep something in sync? If we did this, things like |
This comment has been minimized.
This comment has been minimized.
I mean, that doesn't mean it's good just because someone else does it. |
This comment has been minimized.
This comment has been minimized.
|
Oh, like this eval = case _ of
Rerender next -> do
st <- H.get
H.query (SProxy :: SProxy "child") unit $ H.action $ DoSomething st.usedToBeInput
pure next
... |
This comment has been minimized.
This comment has been minimized.
|
Doesn't that defeat the point of "declarative", if we have to imperatively update everything to keep it in sync? |
This comment has been minimized.
This comment has been minimized.
|
I have no problem adding a Rerender hook, btw. I just don't think it's a great replacement for inputs. |
This comment has been minimized.
This comment has been minimized.
|
Well, querying isn't declarative too :) Inputs are just queries, right? We send
But this is not so bad idea too. This approach is very old and solid. What I'm trying to say is that we have too much public interfaces in component. And too much type parameters. If we want to introduce lifecycle algebra, then we could take a chance to remove at least one type parameter. Actually, |
This comment has been minimized.
This comment has been minimized.
I'm not saying that there shouldn't be imperative querying. I'm saying that only having imperative querying is not good, and makes the library strictly less useful. If we are using a pure functional language, why would we make imperative, effectful updates the only means of communication? I just don't see why this would be desirable.
It is sugar, but it's not sugar you can easily recover if you remove it from the library. Could you tell me how I could realistically recover declarative updates if we removed the sugar from the library?
My point is that it isn't a goal of Halogen to conform to Erlang, so saying "This is how Erlang does it" isn't a motivation or reason to accept a proposal. If we remove |
This comment has been minimized.
This comment has been minimized.
|
I agree, having to mess with UUIDs/AVars whatnot to match request/responses is always worse when a synchronous interface is possible instead. |
This comment has been minimized.
This comment has been minimized.
I'm not suggesting we do it at all, I'm saying if we were pushing to cut down the communications options (not that I think we should particularly), I dislike inputs-as-queries enough that I'd rather lose inputs than queries. |
This comment has been minimized.
This comment has been minimized.
Because communication is effectful and imperative, right?
I couldn't of course, because it's almost impossible. I agree that w/o
I didn't say that because it's like in Erlang, I said that because it's not FRP.
Not necessary request :: i -> HalogenM f s ps o m (Maybe o)
eval = case _ of
ToggleButton next -> do
request CheckButtonState >>= case _ of
Just (Response a) -> doSomething
_ -> pure unit
pure next |
This comment has been minimized.
This comment has been minimized.
I don't fully understand what point you are trying to make. Can you clarify?
Maybe it's not just sugar then, if it isn't something you can recover? I would say there is a difference. Take for example a list of components. Right now there is a single (declarative) source of truth for coordinating a component's position and parameters. A post-render hook would mean I now have to split that logic into two and make two traversals manually, which just introduces the opportunity for them to drift. I don't agree that these are "the same". You can maybe achieve the same end-result, but post-hook is clearly much more brittle and inefficient. request :: i -> HalogenM f s ps o m (Maybe o)This is not equivalent to a request though. Requests have well-typed responses specifically so you don't have to deal with partiality and the bugs it introduces. By only making the result |
This comment has been minimized.
This comment has been minimized.
Nevermind, I described different concept with word Hm... You've convinced me about preserving
I still think that there is a way of unification |
This comment has been minimized.
This comment has been minimized.
|
I'd say there's a semantic difference between what
We could definitely take a more fundamentalist approach to how some of these things work, removing forking and such so that every I think simplicity of implementation is a nice goal, but for a library like this, where there are a wide variety of uses and no real strong underlying model that covers all of them naturally, providing an interface that provides easy ways to do things the "right" way (in our opinion - derived from experience of doing it other ways and finding them harder to work with) is the better way to go. Perhaps some unifications will arise again in the future that result in things being simplified again (I have some ideas that I'd like to experiment with that would reduce or maybe erase the interface difference between elements and child components, for example). But as long as we don't end up with a sprawling interface with many different entirely equivalent ways to solve the same problem, I think we're still good. |
This comment has been minimized.
This comment has been minimized.
|
And FWIW, I agree that it would be nice to have fewer type variables, but with this proposal it's not any more unique parameters than what we currently have since we are removing one by using rows for children. I also agree that we should bikeshed parameter order some. I also don't think that this fundamentally makes anything more complex. In fact it makes a lot of use cases far simpler since inputs don't have to be functors. With rows for slots, component modules can export their own slot constructors, so it's easy to write simpler |
This comment has been minimized.
This comment has been minimized.
|
In reference to this specific proposal, I'd really like to hear some ideas around handling the partiality of proxied requests. I don't like that it makes top-level requests partial, but it may be inevitable since you can |
This comment has been minimized.
This comment has been minimized.
|
Could we have |
This comment has been minimized.
This comment has been minimized.
|
The problem I have with that is that it special cases the root component. That would mean if I had a root component, and then decided to wrap it in some way, I would have to go back and change it so that it works under MaybeT. |
This comment has been minimized.
This comment has been minimized.
|
One thing we could do is just keep the root request as "total", and insert a |
This comment has been minimized.
This comment has been minimized.
|
Yeah, that sounds reasonable to me... so |
This comment has been minimized.
This comment has been minimized.
|
Yes, but we can also just go ahead and return a |
This comment has been minimized.
This comment has been minimized.
|
Bike-shedding parameter order: data Component g m i o
data HalogenQ g i a
data HalogenM s ps f o m a
data HTML ps f
-- Encourage this type synonym which has parameters in the
-- same order as HTML and Component.
type HalogenEval s ps f g m i o = HalogenQ g i ~> HalogenM s f ps o m |
This comment has been minimized.
This comment has been minimized.
|
Don't we need the parameter for the I'd vote the |
This comment has been minimized.
This comment has been minimized.
Yes, of course. I knew I was forgetting something.
I would too, but I think the problem is the |
This comment has been minimized.
This comment has been minimized.
|
Damn, you're right. I'll take another look at this later today. |
This comment has been minimized.
This comment has been minimized.
paulyoung
commented
Jun 6, 2018
|
I'm excited about this |
natefaubion commentedApr 28, 2018
Currently the component query algebra is part of the public interface of a component. This makes it difficult to implement transparent HOCs because the types become infectious. I propose that we existentially hide a component's query algebra.
In #515 I proposed adding a separate library algebra for lifecycle events:
I'm going to propose we actually just make this
evaland add two more constructor so that it looks like this.This takes the
Halogen.Component.Proxyfunctionality and makes it first class. We separate a public query algebra from an internal query algebra which means type parameters are no longer infectious or invariant. We can even recover the existing component spec definitions in terms of this by makingf ~ g.Using this we can write an encapsulated HOC where no extra types leak.
Note I used
External (g (Maybe a))which means external queries can be partial. This is always the case anyway since a component may not be mounted, but this allows the inner component to control that partiality as well.