diff --git a/docs/docs.ipkg b/docs/docs.ipkg index cedd097..4c66348 100644 --- a/docs/docs.ipkg +++ b/docs/docs.ipkg @@ -3,8 +3,9 @@ version = 0.0.1 authors = "Stefan Höck" depends = dom-mvc , contrib - , rio + , finite , monocle + , rio opts = "--codegen javascript" diff --git a/docs/src/Examples/Balls.md b/docs/src/Examples/Balls.md index d6d735f..62fe091 100644 --- a/docs/src/Examples/Balls.md +++ b/docs/src/Examples/Balls.md @@ -150,8 +150,8 @@ milliseconds. ```idris public export data BallsEv : Type where - Init : BallsEv - Run : BallsEv + BallsInit : BallsEv + Run : BallsEv NumIn : Either String Nat -> BallsEv Next : DTime -> BallsEv @@ -279,7 +279,7 @@ showFPS n = in "FPS: \{show val}" adjST : BallsEv -> BallsST -> BallsST -adjST Init _ = init +adjST BallsInit _ = init adjST Run s = {balls := maybe s.balls initialBalls s.numBalls} s adjST (NumIn x) s = {numBalls := eitherToMaybe x} s adjST (Next m) s = case s.count of @@ -294,7 +294,7 @@ displayST s = ] displayEv : BallsEv -> DOMUpdate BallsEv -displayEv Init = child exampleDiv content +displayEv BallsInit = child exampleDiv content displayEv Run = NoAction displayEv (NumIn x) = validate txtCount x displayEv (Next m) = NoAction @@ -304,8 +304,8 @@ display e s = displayEv e :: displayST s export runBalls : Has BallsEv es => SHandler es -> BallsEv -> BallsST -> JSIO BallsST -runBalls h Init s = do - s2 <- injectDOM adjST display h Init s +runBalls h BallsInit s = do + s2 <- injectDOM adjST display h BallsInit s stop <- animate (h . inject . Next) pure $ {cleanUp := stop} s2 runBalls h e s = injectDOM adjST display h e s diff --git a/docs/src/Examples/CSS.md b/docs/src/Examples/CSS.md index 67adc92..a3b9f03 100644 --- a/docs/src/Examples/CSS.md +++ b/docs/src/Examples/CSS.md @@ -43,8 +43,8 @@ be found in the corresponding submodules). ```idris export -allRules : List (Rule 1) -allRules = +rules : List (Rule 1) +rules = coreCSS ++ Balls.css ++ Fractals.css diff --git a/docs/src/Examples/Fractals.idr b/docs/src/Examples/Fractals.idr index 64d26eb..1d61404 100644 --- a/docs/src/Examples/Fractals.idr +++ b/docs/src/Examples/Fractals.idr @@ -54,11 +54,11 @@ readDelay = public export data FractEv : Type where - Init : FractEv - Iter : Either String Iterations -> FractEv - Redraw : Either String RedrawDelay -> FractEv - Run : FractEv - Inc : DTime -> FractEv + FractInit : FractEv + Iter : Either String Iterations -> FractEv + Redraw : Either String RedrawDelay -> FractEv + Run : FractEv + Inc : DTime -> FractEv public export record FractST where @@ -108,7 +108,7 @@ rotate [] = [] rotate (h::t) = t ++ [h] adjST : FractEv -> FractST -> FractST -adjST Init s = init +adjST FractInit s = init adjST (Iter x) s = {itersIn := eitherToMaybe x} s adjST (Redraw x) s = {redrawIn := eitherToMaybe x} s adjST (Inc dt) s = @@ -137,7 +137,7 @@ displayST s = ] displayEv : FractEv -> DOMUpdate FractEv -displayEv Init = child exampleDiv content +displayEv FractInit = child exampleDiv content displayEv (Iter x) = validate txtIter x displayEv (Redraw x) = validate txtRedraw x displayEv Run = NoAction @@ -148,8 +148,8 @@ display e s = displayEv e :: displayST s export runFract : Has FractEv es => SHandler es -> FractEv -> FractST -> JSIO FractST -runFract h Init s = do - s2 <- injectDOM adjST display h Init s +runFract h FractInit s = do + s2 <- injectDOM adjST display h FractInit s stop <- animate (h . inject . Inc) pure $ {cleanUp := stop} s2 runFract h e s = injectDOM adjST display h e s diff --git a/docs/src/Examples/MathGame.md b/docs/src/Examples/MathGame.md index 0c3f4db..e13e3cc 100644 --- a/docs/src/Examples/MathGame.md +++ b/docs/src/Examples/MathGame.md @@ -44,10 +44,10 @@ data Language = EN | DE public export data MathEv : Type where - Lang : Language -> MathEv - Check : MathEv - Init : MathEv - Inp : String -> MathEv + Lang : Language -> MathEv + Check : MathEv + MathInit : MathEv + Inp : String -> MathEv lang : String -> MathEv lang "de" = Lang DE @@ -214,7 +214,7 @@ content l = ] [Text $ checkAnswerStr l] , button [ Id newBtn - , onClick Init + , onClick MathInit , classes [widget,btn] ] [Text $ newGameStr l] @@ -324,7 +324,7 @@ the input text field: adjST : MathEv -> MathST -> MathST adjST (Lang x) = {lang := x} adjST Check = checkAnswer -adjST Init = id +adjST MathInit = id adjST (Inp s) = {answer := s} displayST : MathST -> List (DOMUpdate MathEv) @@ -341,7 +341,7 @@ displayST s = displayEv : MathEv -> DOMUpdate MathEv displayEv (Lang x) = child exampleDiv (content x) displayEv Check = Value resultIn "" -displayEv Init = child exampleDiv (content init.lang) +displayEv MathInit = child exampleDiv (content init.lang) displayEv (Inp _) = NoAction display : MathEv -> MathST -> List (DOMUpdate MathEv) @@ -349,7 +349,7 @@ display e s = displayEv e :: displayST s export runMath : Has MathEv es => SHandler es -> MathEv -> MathST -> JSIO MathST -runMath h Init s = randomGame s.lang >>= injectDOM adjST display h Init +runMath h MathInit s = randomGame s.lang >>= injectDOM adjST display h MathInit runMath h e s = injectDOM adjST display h e s ``` diff --git a/docs/src/Examples/Performance.md b/docs/src/Examples/Performance.md index 3980efc..78dd04d 100644 --- a/docs/src/Examples/Performance.md +++ b/docs/src/Examples/Performance.md @@ -64,7 +64,7 @@ PosNat = Subset Nat IsSucc public export data PerfEv : Type where - Init : PerfEv + PerfInit : PerfEv NumChanged : Either String PosNat -> PerfEv Reload : PerfEv Set : Nat -> PerfEv @@ -188,7 +188,7 @@ convenient to use. ```idris adjST : PerfEv -> PerfST -> PerfST -adjST Init = const init +adjST PerfInit = const init adjST (NumChanged e) = {num := eitherToMaybe e} adjST Reload = {sum := 0} adjST (Set k) = {sum $= (+k)} @@ -197,7 +197,7 @@ displayST : PerfST -> List (DOMUpdate PerfEv) displayST s = [disabledM btnRun s.num, show out s.sum] displayEv : PerfEv -> PerfST -> DOMUpdate PerfEv -displayEv Init _ = child exampleDiv content +displayEv PerfInit _ = child exampleDiv content displayEv (NumChanged e) _ = validate natIn e displayEv (Set k) _ = disabled (btnRef k) True displayEv Reload s = maybe NoAction (child buttons . btns) s.num diff --git a/docs/src/Examples/Reset.md b/docs/src/Examples/Reset.md index 44559f3..b276093 100644 --- a/docs/src/Examples/Reset.md +++ b/docs/src/Examples/Reset.md @@ -38,8 +38,8 @@ our event type will be a function on integers: ```idris public export data ResetEv : Type where - Init : ResetEv - Mod : (Int8 -> Int8) -> ResetEv + ResetInit : ResetEv + Mod : (Int8 -> Int8) -> ResetEv ``` ## View @@ -101,7 +101,7 @@ that produces no output of interest): ```idris adjST : ResetEv -> Int8 -> Int8 -adjST Init n = 0 +adjST ResetInit n = 0 adjST (Mod f) n = f n displayST : Int8 -> List (DOMUpdate ResetEv) @@ -113,8 +113,8 @@ displayST n = ] display : ResetEv -> Int8 -> List (DOMUpdate ResetEv) -display Init n = child exampleDiv content :: displayST n -display (Mod f) n = displayST n +display ResetInit n = child exampleDiv content :: displayST n +display (Mod f) n = displayST n export runReset : Has ResetEv es => SHandler es -> ResetEv -> Int8 -> JSIO Int8 diff --git a/docs/src/Examples/Selector.md b/docs/src/Examples/Selector.md index ab6c6cb..1f13b40 100644 --- a/docs/src/Examples/Selector.md +++ b/docs/src/Examples/Selector.md @@ -1,7 +1,7 @@ -# The Basic Layout of a rhone-js Web Page +# The Basic Layout of a dom-mvc Web Page This module defines the HTML structure of the main page and -implements the functionality of the `select` element that is +implements the functionality of the `` element at the top of the +page is used to select one of several example applications. We list +the possible values in an enumeration and provide a function for +converting the string inputs from the user interface to the +correct event value: ```idris public export -data App = Reset | Perf | Balls | Fract | Math +data AppEv = Reset | Perf | Balls | Fract | Math + +%runElab derive "AppEv" [Show,Eq,Finite] + +toApp : String -> AppEv +toApp s = fromMaybe Reset $ find ((s ==) . show) values +``` + +Here is the layout of the main page: -toApp : String -> App -toApp "perf" = Perf -toApp "balls" = Balls -toApp "fract" = Fract -toApp "math" = Math -toApp _ = Reset +```idris +appName : AppEv -> String +appName Reset = "Counting Clicks" +appName Perf = "Performance" +appName Balls = "Bouncing Balls" +appName Fract = "Fractals" +appName Math = "Math Game" + +opt : AppEv -> Node AppEv +opt v = option [value (show v), selected (v==Reset)] [Text $ appName v] -content : Node App +content : Node AppEv content = div [ class contentList ] [ div [class pageTitle] ["dom-mvc: Examples"] , div [class contentHeader] [ label [class widgetLabel] ["Choose an Example"] , select - [ classes [widget, selectIn, exampleSelector], onChange toApp] - [ option [ value "reset", selected True ] ["Counting Clicks"] - , option [ value "performance" ] ["Performance"] - , option [ value "fractals" ] ["Fractals"] - , option [ value "balls" ] ["Bouncing Balls"] - , option [ value "math" ] ["Math Game"] - ] + [classes [widget, selectIn, exampleSelector], onChange toApp] + (map opt values) ] , div [Id exampleDiv] [] ] @@ -77,22 +103,35 @@ content = A typical `Node` constructor like `div` or `label` takes two arguments: A list of attributes and a list of child -nodes. +nodes. We use these to describe the tree structure of a HTML page. +This works very well, as the code is typically quite readable, while +we still have the power of Idris at hand: The options of the select +element come from applying function `opt` to all `AppEv` values. +(Function `values` comes from interface `Data.Finite.Finite` from the +finite library.) Several things need some quick explanation: CSS classes like `pageTitle` or `contentHeader` are just `String`s defined in module `Examples.CSS` together with the corresponding CSS rules. -(If you are new to web development: [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS) +(If you are new to web development: +[CSS](https://developer.mozilla.org/en-US/docs/Web/CSS) is a domain specific language used to describe the presentation of documents written in HTML and similar markup languages. It is used to define the appearance of the example web page.) - -DOM identifiers like `exampleDiv` are of type `ElementRef t` (defined -in `Rhone.JS.ElemRef`), where `t` is the type of the -corresponding HTML element. They are mainly typed wrappers around ID -strings and are used to lookup HTML elements in the DOM. -We need these, whenever an element in the DOM is not static. +In this example application I show how to define CSS rules in Idris +using the data types and functions from `Text.CSS` and its submodules. +If you prefer to use plain `.css` files instead (as I do nowadays), +that's perfectly fine as well. + +DOM identifiers like `exampleDiv` are of type `Ref t` (defined +in `Text.HTML.Ref`), where `t` is a tag of type `Text.HTML.Tag.HTMLTag` +corresponding to the HTML element's tag. They are mainly typed wrappers +around ID strings and are used to lookup HTML elements in the DOM. +Tag `t` gives us some additional guarantees, both when building +a `Node` (the element and ID tag must match), but also when looking +up elements in the DOM, or when we need to restrict the allowed +elements in a function. In the example above, `exampleDiv` points to the DOM element where the content of the example applications will go. It's the only part of the main web page that is not static. @@ -100,62 +139,82 @@ only part of the main web page that is not static. Finally, we also encode the events an element fires in the `Node` type, and that's what `Node`'s parameter stands for. Events are just attributes, and in the example above, the `select` element -fires an event whenever the user changes the selected value -(`onChange id`). +fires an `AppEv` event whenever the user changes the selected value +(`onChange toApp`). -## The Interactive Part: Monadic Stream Functions +## The Interactive Part: Handling Events Now that we have the structure of our web page specified, we can have a quick look at how we define its interactive behavior. -rhone-js is named after [idris2-rhone](https://github.com/stefan-hoeck/idris2-rhone) -a port of monadic stream functions (MSF) first introduced -in Haskell's [dunai](https://hackage.haskell.org/package/dunai) -library and explained in detail in a nice -[article](https://www.cs.nott.ac.uk/~psxip1/#FRPRefactored). -This is probably the most accessible implementation of (arrowized) -functional reactive programming I have so far come across. - -In its most general form, an MSF can be thought of -as having the following type: +The core function used for this (which is used in our application's +`main` function) is `Web.MVC.runMVC`. Here's its type ```haskell -data MSF m i o = MSF (i -> m (o, MSF m i o)) +runMVC : + {0 e,s : Type} + -> (initEv : e) + -> (initST : s) + -> (modST : Handler e -> Controller s e) + -> JSIO () ``` -This is an effectful computation over a monad type `m` converting -an input value of type `i` to an output value of type `o` plus -a new MSF (the original MSF's continuation), which will be used -to convert the next piece of input. - -In the *rhone* library, we don't use this most general form, -as it does not go well with the totality checker. Instead, -our implementation of `MSF` encodes a rather large set -of primitive operations in the data type itself. This gives -us the benefit of provably total stream functions, which is -extremely valuable especially when we start using advanced -components like event switches. - -`MSF` implements interfaces `Functor` and `Applicative` but also -`Category` and `Arrow`. It lets us lift any effectful function -into an `MSF` value via function `arrM` (used in the code below), -supports feedback loops for stateful computations and several -kinds of event switches to dynamically change the behavior -of a web page. The ability to lift arbitrary effectful computations -is especially useful, as it allows us to update the DOM -directly from within an MSF. - -Enough talk, here's the code: +We are going to describe the different parts in detail: An interactive +web application fires and handles events from user input. In the type +above, `e` is the event type, and `initEv` is the initial event +we fire when starting the application. + +Non-trivial web pages are stateful: An event modifies the application +state (for instance, by increasing a counter when a button is clicked) +and the new state is used to update what we see in the user interface. +The state type in `runMVC` is represented by parameter `s`, and `initST` +is the initial application state. + +Finally, we need a way to update and display the state when an event +occurs, and this is what function `modST` does. Now, this is the +most complex argument, so we need to talk about it in some detail. +The first argument of `modST` is of type `Handler e`, which is an +alias for `e -> JSIO ()`. So `modST` is given a function for handling +events, which it can use to register event listeners at the user interface. +As we will see, we don't typically register event handlers manually, +as the utilities we invoke do this for us. However, they need access +to an event handler to do so, and that's what the first argument of `modST` +is used for. + +The result type `Controller s e` is an alias for `e -> s -> JSIO s`, +so `modST` actually takes two more arguments. +These are the current event and application state, respectively, +and the result is a new application state plus +some side effects for updating the user interface. + +As we will see, we often use pure functions together with some +existing utilities for `modST`, which will tremendously simplify +our code. Still, for the main application, we are going to need +the components described above, so let's define them. + +First, the event type. Our main application consists of a selection +of several unrelated example apps, each with its own state and +event type. We collect all event types plus our own `AppEv` in +a heterogeneous sum, and use this as the main event type of our +application: ```idris public export 0 Events : List Type -Events = [App, BallsEv, FractEv, PerfEv, ResetEv, MathEv] +Events = [BallsEv, FractEv, PerfEv, ResetEv, MathEv] public export 0 SelectEv : Type -SelectEv = HSum Events +SelectEv = HSum (AppEv :: Events) +``` + +Likewise, we use a record type listing the states of the different +applications in its fields. We are going to use the lenses from +monocle to modify a single application state, depending on the +input being fired. We also define the initial application state, +which just lists the initial state of each example application. +```idris public export record ST where constructor S @@ -170,65 +229,73 @@ record ST where export init : ST init = S init 0 init init init +``` -0 Controller : Type -> Type -Controller e = e -> ST -> JSIO ST - -runSelect : Handler SelectEv -> App -> JSIO ST -runSelect h Perf = modifyA perfL (runPerf h Init) init -runSelect h Balls = modifyA ballsL (runBalls h Init) init -runSelect h Fract = modifyA fractL (runFract h Init) init -runSelect h Math = modifyA mathL (runMath h Init) init -runSelect h Reset = do - updateDOM (h . inject) - [ style appStyle allRules , child contentDiv content ] - modifyA resetL (runReset h Init) init - -cleanup : ST -> JSIO () -cleanup s = liftIO (s.balls.cleanUp >> s.fract.cleanUp) - -controllers : (SelectEv -> JSIO ()) -> All Controller Events -controllers h = - [ \e,s => cleanup s >> runSelect h e - , modifyA ballsL . runBalls h - , modifyA fractL . runFract h - , modifyA perfL . runPerf h - , modifyA resetL . runReset h - , modifyA mathL . runMath h - ] +We can now implement the main application controller. In order to +distinguish between the events coming from each application, +we use a heterogeneous list holding on controller for each event type. +Function `Web.MVC.controlMany` is useful for this. All controllers +are going to need access to an event handler, so we pass this via +a `parameters` block: -export -ui : (SelectEv -> JSIO ()) -> SelectEv -> ST -> JSIO ST -ui h es s = collapse' $ hzipWith (\f,e => f e s) (controllers h) es +```idris +parameters (h : Handler SelectEv) + + runApp : Controller ST (HSum Events) + runApp = + controlMany + [ modifyA ballsL . runBalls h + , modifyA fractL . runFract h + , modifyA perfL . runPerf h + , modifyA resetL . runReset h + , modifyA mathL . runMath h + ] +``` + +Function `runApp` only handles the events from example applications, +but not the `AppEv` event from the main `` element: + +```idris + changeApp : Controller ST AppEv + changeApp Perf = runApp (inject PerfInit) + changeApp Balls = runApp (inject BallsInit) + changeApp Fract = runApp (inject FractInit) + changeApp Math = runApp (inject MathInit) + changeApp Reset = runApp (inject ResetInit) +``` + +As we will see when we look at each of the example applications, every +one of them sets up its own HTML nodes upon receiving its `Init` event. + +To sum it all up, and to define function `ui`, which is directly invoked +by function `main`, we just pattern match on the event we get. If it's +an `AppEv` signalling that the user wants to try a different example +app, we cleanup and switch applications. If it's coming from a running +application, however, we just pass it on to `runApp`. +```idris + export + ui : Controller ST SelectEv + ui (Here x) s = cleanup s >>= changeApp x + ui (There x) s = runApp x s +``` ## Comparison with other MVC Libraries @@ -246,10 +313,13 @@ for instance the one used by the *Elm* programming language, make use of a *virtual DOM*. This is an in-memory model of the real DOM used by the browser, and this is the *view* that is being manipulated in Elm applications. On each -update, the virtual DOM is compared to its previous state +event, the model (state) is updated based on the current event, +the view (virtual DOM) is updated to display the new state, and +the virtual DOM is compared to its previous version (a process called *DOM diffing*) and the real DOM is updated to reflect the changes made to the virtual DOM. The advantage of this -approach is that we can write a single (pure!) function for converting +approach is that we can often write pretty simple and pure functions +for updating the model and converting the model to the view and never have to interact with the real DOM explicitly. The downside is, that we loose some control over which parts of the web page are updated when, @@ -257,11 +327,9 @@ which can have an impact on performance, especially when the web page - and thus the virtual DOM - consists of many elements. -So far, rhone-js does not use a virtual DOM but interacts with -the real DOM directly through a network of monadic stream -functions. Whether this will result in a nice way to write web applications -or will lead to unmaintainable tangles of code, only time -and experience will tell. +So far, dom-mvc does not use a virtual DOM but uses both +the current event and updated state to determine, which parts of +the DOM should be modified when and how. ## Whats next? diff --git a/src/Web/MVC.idr b/src/Web/MVC.idr index 5f2f0f0..78ead9f 100644 --- a/src/Web/MVC.idr +++ b/src/Web/MVC.idr @@ -1,18 +1,41 @@ module Web.MVC import Data.IORef +import Data.List.Quantifiers.Extra import public JS import public Web.MVC.DOMUpdate import public Web.MVC.Reactimate import public Text.HTML +||| A controller is an effectful computation for +||| updating a displaying some application state based +||| on an event type e. +||| +||| When dealing with a heterogeneous sum of possible events, +||| as is encouraged here for writing applications with several +||| more or less unrelated event sources, it is convenient +||| to wrap one controller for handling each event in a +||| heterogeneous list from `Data.List.Quantifiers`. Therefore, +||| the state type comes first in this alias, and the event +||| type comes second, even though in actual controller function +||| it's the other way round. +public export +0 Controller : Type -> Type -> Type +Controller s e = e -> s -> JSIO s + +||| Given a heterogeneous list of controllers, we can react +||| on the events in a heterogeneous sum. +export +controlMany : All (Controller s) es -> Controller s (HSum es) +controlMany cs evs s = collapse' $ hzipWith (\f,e => f e s) cs evs + export covering runMVC : - {0 e,s : Type} - -> (initEv : e) - -> (initST : s) - -> (modST : Handler e -> e -> s -> JSIO s) + {0 e,s : Type} + -> (initEv : e) + -> (initST : s) + -> (modST : Handler e -> Controller s e) -> JSIO () runMVC initEv initST modST = do ref <- newIORef initST