mikesol / purescript-deku

A PureScript web UI framework
https://purescript-deku.surge.sh/
Apache License 2.0
123 stars 12 forks source link

Make providers an actual feature #104

Closed jterbraak closed 10 months ago

jterbraak commented 10 months ago

So the documentation recognizes the need for implicit propagation of application state. And while it does give some solutions I don't think it's all that implicit, requiring a left bind per "component". It also forces mixing do levels, which the documentation also notices as a problem. To solve this I would like to change the signature of Nut something like env -> ANut and Attribute to env -> { key :: String, value :: AttributeValue }. All elements and attributes provided by the DOM modules and control functions like switcher and guard receive this new signature and compose. The internal ANut would stay as is. The DOM-tree values would propagate the env to their children and Attributes. The runIn... functions then get a parameter to inject the initial env. Although you could always just apply the env yourself and change the signature to only run a ANut. Adding a Contravariant instance would add the ability to hoist Nuts between environments via cmap if they don't fit in the suggested Newtype Record model. The provided hooks would have to change as well of course. Maybe add a useEnv or useContext to get explicit access to the env.

mikesol commented 10 months ago

It's an interesting idea and has come up before.

I'm still on the fence about it. Check out, for example, this draft of the new deku docs: https://righteous-rain.surge.sh/. Go to the Effect section and scroll all the way down to the end-ish section for game developers, where there's a little game. You can click Start Game. The goal is to try to click the circles before they disappear. It's maddening.

This uses a more elaborate effect system than a provider: one that essentially creates a game engine. That said, the whole example is only 300 loc, so making these things doesn't take that long once you get the hang of it.

When I've used deku to make games and instruments, I always wind up doing some variation on this & create a custom effect system. The documentation also uses this strategy. Check out https://github.com/mikesol/deku-documentation/blob/7c91f3cb4269af8891edf500c91a75d0e459fac1/src/Contracts.purs#L102, for example. That's the way pages get subbed in and out and cancel any ongoing effects in case there are any (like, for example, if a page has started a timer in one of the examples). This is a free monad, and you can see that one of the branches in the ADT is GetEnv, which is a provider.

My concern with building providers into the framework is that it commits to one particular effect system - the reader monad - instead of letting people build their own. We could theoretically provide starter effects systems for folks, but I've found that when I start a new project, I start with the effect system and let that guide how the game/instrument develops. So I don't know how much mileage prefabbed effect systems would give people. But I'm not opposed to it if it's useful! Just stating my bias based on my own usage of the framework.

jterbraak commented 10 months ago

So the discussion is between between having Nut be the base abstraction of your application or something custom made. Initially I considered every component of an application to be some sort of Nut or a -> Nut and your argument is that it's probably a better idea to build your own types on top of Nut. I wanted to build a more consistent subset of HTML anyway ala elm-ui so it's an approach worth trying out.

One typing I did try Array ( f ( Poll ( Attribute r ) ) ) -> Array ( f Nut ) -> f Nut which has some interesting properties and works surprisingly not awful:


data ContextF a
    = GetEnv ( Env -> a )
    | GetRandom ( Number -> a )
    | MakeStyle String ( Attribute' -> a )

...

elementify3 :: forall f r . Monad f => String -> Array ( f ( Poll ( Attribute r ) ) ) -> Array ( f Nut ) -> f Nut
elementify3 tag fattr fchildren = do
    attr <- sequence fattr
    children <- sequence fchildren
    pure $ elementify2 Nothing tag attr children

...
main :: Effect Unit
main = do
    setPresence /\ presence <- liftST $ DE.useState false

    app /\ style <- runWriterT $ foldFree ( interp { title : "some title" } ) do
        li []
            [ div [ makeStyle "background-color:red;" ] [ text_ <<< _.title <$> getEnv ]
            , map ( guard presence ) $ div [ makeStyle "color:red;", makeStyle "background-color:blue;" ]
                [ text_ <<< show <$> getRandom ]

            , button [ pure $ DL.click $ presence <#> \p _ -> setPresence $ not p ] [ pure $ text_ "hide" ]
            ]

    runInBody $ fixed [ app, D.style_ [ text_ style ] ]

This naively collects all styles and renders them at the bottom in a stylesheet. Although I haven't figured out how to make switcher work yet.

mikesol commented 10 months ago

Do I understand correctly that your goal is to create a style sheet for all styles that are present on an initial render? If so, a couple questions: