seanhess / hyperbole

Haskell interactive serverside web framework inspired by HTMX
Other
93 stars 6 forks source link

Simplify handle / load interface design #31

Closed benjaminweb closed 3 weeks ago

benjaminweb commented 1 month ago

Current handle load interface goes like

handle central $ handle presets $ handle handler3 $ load $ do

A handler's function is to catch any actions originating from any views. handle currently wraps a handler around other handlers so the actions get caught. A handler will not work

Current architecture:

Desirable qualities of new handle / load interface design:

seanhess commented 1 month ago

What design would be an improvement on what we have now?

Here are good options that aren't possible:

  1. Lists (not heterogenous)
  2. Monadic interface, like in the previous version (same problem, all the types need to be the same. can't build up a type one handler at a time)

Other Options I considered

Operators - we could create operators that mimic what handle and load are doing. Something like this (Using Example.Contacts)

-- I'm not saying these are the best operators, just showing what might be possible. 
page :: (Hyperbole :> es, Users :> es, Debug :> es) => Page es '[Contacts, Contact]
page = do
 contacts :|: handle contact :$: do
    ...

Type Operators - Similar to Servant, use type operators instead of a type-level list. Then use the same operators to link the handlers together.

page :: (Hyperbole :> es, Users :> es, Debug :> es) => Page es (Contacts :|: Contact)
page = do
 contacts :|: handle contact :$: do
    ...

Tuples - We could make use tuples instead? They get unweidly with a bunch of handlers though! But that could be solved with some sort of factoring / nested tuples?

-- we could keep the type-level list, or a use a type-level tuple
page :: (Hyperbole :> es, Users :> es, Debug :> es) => Page es (Contacts, Contact)
page = do
 handle (contacts, contact) $ do 
  ...

Have any other ideas?

benjaminweb commented 1 month ago

When would we not need this at all? I mean, is there any way to register those handlers via instances (or any other way) so they get automatically pulled in? You do that already for actions but why not for the handlers on the Page level? I might be completely off-track here, but just want to bring this up.

seanhess commented 1 month ago

Good question. If they are handled explicitly, you can easily pass app context to a handler, or more importantly, to reuse hyperviews

Context - as in a db connection or something. This isn't that useful, since it's easy to use the Reader to accomplish the same thing

myHandler :: Connection -> MyView -> MyAction -> Eff es (View MyView ())
myHandler conn _ Save = Db.saveSomething "hello"

More likely, you might want to reuse a hyperview to do different things. Here's an example from a [real application])(https://github.com/DKISTDC/level2/blob/main/src/App/Page/Program.hs#L38):

inversions :: (Hyperbole :> es, ...) => Eff es (View InversionStatus ()) -> InversionStatus -> InversionAction -> Eff es (View InversionStatus ())
inversions onCancel (InversionStatus inversionId) = \case
  Cancel -> do
    send $ Inversions.Remove inversionId
    onCancel

 page
  :: (Hyperbole :> es, Log :> es, Inversions :> es, Datasets :> es, Auth :> es, Globus :> es, Tasks GenFits :> es)
  => Id Proposal
  -> Id Inversion
  -> Page es [InversionStatus, ...]
page ip i = do
  handle (inversions redirectHome) $ ...

redirectHome :: (Hyperbole :> es) => Eff es (View InversionStatus ())
redirectHome = do
  redirect $ pathUrl . routePath $ Inversions

The inversions handler can be reused on different pages with a different effect for onCancel. Here is the same handler used on a different page

page
  :: (Hyperbole :> es, ...)
  => Id Proposal
  -> Id InstrumentProgram
  -> Page es [InversionStatus, ...]
page ip iip = do
  handle (inversions (clearInversion ip iip)) $ ...

Maybe reusing views like this is a bad idea. I'm not sure. It definitely came up in an app of mine. Maybe a user could "newtype" a hyperview instead for reuse?

There may have been another technical reason why that wouldn't work. I can't remember. I definitely considered it. It would be pretty neat to have the typeclass contain the handler, I think. Maybe it's worth the hassle, if we can come up with another way to reuse the views

benjaminweb commented 1 month ago

Do I get you right, we have two things that exclude each other:

btw I didn't realise that it's possible to reuse views since.

seanhess commented 1 month ago

Reusing views this way isn't possible, because you couldn't add a parameter to the handler function if it were in a class:

instance HyperView MyView where
  type Action MyView = MyAction
  -- I can't pass anything into this!
  handler view action = ...

That doesn't mean we couldn't come up with different patterns for view reuse.

seanhess commented 3 weeks ago

@benjaminweb I spent a day trying to figure out how to resolve the handlers via the typeclass. I can't figure out how to make it work with effects

But I had an idea: we can't use lists, but what about tuples?

page :: (Hyperbole :> es) => Page es '[Contacts, Contact]
page = do
  -- pass a tuple to handle with all your handlers. This resolves to a Page with the proper handlers. 
  handle (contacts, contact) $ do
    us <- usersAll
    pure $ do
      col (pad 10 . gap 10) $ do
        hyper Contacts $ allContactsView Nothing us
seanhess commented 3 weeks ago

Fixed via #41