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

Use `RowCons` for child components #439

Closed
natefaubion opened this Issue Apr 7, 2017 · 4 comments

Comments

3 participants
@natefaubion
Copy link
Collaborator

natefaubion commented Apr 7, 2017

With RowCons, it's possible to define a mapping for child components without all the coproduct/either cruft. This should be more efficient and ergonomic because child queries and state can be dispatched directly without any wrapping.

data At ix (gType  Type)

type ChildSlots =
  ( navBarAt Unit Nav.Query
  , itemAt Item.Id Item.Query
  )

data HalogenM s (fType  Type) (c ∷ # Type) (mType  Type) a = ...

query
    sym s f g ix r1 r2 m a
  . RowCons sym (At ix g) r1 r2
   IsSymbol sym
   Ord ix
   SProxy sym
   ix
   g a
   HalogenM s f r1 m (Maybe a)

Internally, we could build a record based on this set of rows, with individual Maps for each component type.

This is great because we can define the Slot -> Query mapping together in a single definition. We had originally wanted to do this with TC magic and type-level assoc lists, but it was quite hideous.

@natefaubion natefaubion added the idea label Apr 7, 2017

@natefaubion

This comment has been minimized.

Copy link
Collaborator

natefaubion commented Apr 7, 2017

We could also add the component Message type in here, which could fix #405

@cryogenian

This comment has been minimized.

Copy link
Member

cryogenian commented Apr 7, 2017

Reimplementing queries with rows could use open variant abstraction, that can be extracted to different library

@natefaubion

This comment has been minimized.

Copy link
Collaborator

natefaubion commented Apr 21, 2018

To elaborate more on the implementation: Currently child component state is managed in a single Map

, children :: M.Map (OrdBox p) (Ref (DriverStateX h r g eff))

This is why ChildPaths are so tedious. We need some way to unify the keys to the map (slots) and some way to unify queries (which are part of the Map values). We currently use two different types for this which we have to keep in sync. What we need is some sort of abstract storage with a Map like interface, but indexed by the rows child slots.

If we start with an empty data decl to represent a slot, we can use this in rows:

-- Indexed by query, output message, and slot key
data Slot (g :: Type -> Type) o k

Then we can define slots for our components as something like:

type ChildSlots =
  ( navigation :: Slot NavQuery NavMessage Unit
  , item :: Slot ItemQuery ItemMessage String
  )

A nice pattern might be components exporting a partially applied slot:

type NavigationSlot = Slot NavQuery NavMessage
type ItemSlot = Slot ItemQuery ItemMessage

type ChildSlots =
  ( navigation :: NavigationSlot Unit
  , item :: ItemSlot String
  )

Which is relatively little boilerplate compared to the existing setup.

Then we need to defined what our storage interface looks like.

-- Storage is parameterized by slot cells
data SlotStorage (slot :: ((Type -> Type) -> Type -> Type -> Type) (# Type)

That is, some storage indexed by child slots with values of some abstract slot which has the same signature as Slot. We can then define a container for driver state much like DriverStateX above.

data DriverSlot h r eff g o k

And then parameterize our storage:

type DriverStorage h r eff = SlotStorage (DriverSlot h r eff)

However, when constructing our component we need to defer instantiation of the slot variable, so we need something like

data SlotStorageConstructor (# Type)

emptySlotStorage :: forall slot cs. SlotStorageConstructor cs -> SlotStorage slot cs

And then change the Component' signature to:

type Component' h s f (cs :: # Type) i o m =
  { initialState :: i -> s
  , render :: s -> h (ComponentSlot h cs m (f Unit)) (f Unit)
  , eval :: f ~> HalogenM s f cs o m
  , receiver :: i -> Maybe (f Unit)
  , initializer :: Maybe (f Unit)
  , finalizer :: Maybe (f Unit)
  , storage :: SlotStorageConstructor cs
  }

Where previously we were capturing the Ord dict with mkOrdBox, now we must capture the SlotStorageConstructor. Then with some sort of constraint like:

class BuildSlotStorage (cs :: # Type) where
  slotStorageConstructor :: RProxy cs -> SlotStorageConstructor cs

We could muster one up while building component. A totally safe implementation of this would use RowToList and Data.Record to maybe build a record of empty Maps, where each label and slot points to a field and Map in that record.

Then in the driver, we could use the constructor to initialize it, and given a map-like interface:

lookupSlot
  :: forall sym cx cs slot g o k
   . RowCons sym (Slot g o k) cx cs
  => IsSymbol sym
  => Ord k
  => SProxy sym
  -> k
  -> SlotStorage slot cs
  -> Maybe (slot g o k)

popSlot
  :: forall sym cx cs slot g o k
   . RowCons sym (Slot g o k) cx cs
  => IsSymbol sym
  => Ord k
  => SProxy sym
  -> k
  -> SlotStorage slot cs
  -> Maybe (Tuple (slot f o k) (SlotStorage slot cs))

insertSlot
  :: forall sym cx cs slot g o slot
   . RowCons sym (Slot g o k) cx cs
  => IsSymbol sym
  => Ord k
  => SProxy sym
  -> k
  -> slot g o k
  -> SlotStorage slot cs
  -> SlotStorage slot cs

We could manage state as we do now.

The difficult part then is capturing all these constraints. Since Halogen uses an initial encoding for everything, we have to put these dictionaries in the appropriate constructors (HH.slot, H.query) where we can then reify them in the driver (like you might do with reifySymbol).

So given a signature for H.query (note it's all the same constraints as our storage interface):

query
    s f sym cx cs g o k m a
  . RowCons sym (Slot g o k) cx cs a
   IsSymbol sym
   Ord k
   SProxy sym
   k
   g a
   HalogenM s f cs m (Maybe a)

We would have to put these constraints in the ChildQuery constructor of HalogenM. This will likely require lots of newtypes and rank-n stuff. I'm not sure what all these would look like, but it seems doable.

@natefaubion

This comment has been minimized.

Copy link
Collaborator

natefaubion commented Apr 21, 2018

One option is to always bundle up the partially applied operations, polymorphic over the slot type.

newtype SlotOperations (cs :: # Type) g o k = SlotOperations
  { lookup :: forall slot. SlotStorage slot cs -> slot g o k
  , pop :: forall slot. k -> SlotStorage slot cs -> Maybe (Tuple (slot g o k) (SlotStorage slot cs))
  , insert :: forall slot. k -> slot g o k -> SlotStorage slot cs -> SlotStorage slot cs
  }
data ComponentSlot h (cs :: # Type) m a = forall g o k i. ComponentSlot (SlotOperations cs g o k) (Component h g i o m) i (o -> a)

@natefaubion natefaubion referenced this issue Apr 24, 2018

Merged

Use rows for child slots #525

5 of 5 tasks complete

@garyb garyb closed this in #525 Jun 6, 2018

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