{-# LANGUAGE ExistentialQuantification #-} {-| Use this library to build @mvc@ applications that consume many individually `Updatable` values, such as: * spread sheets, * control panels, and: * data visualizations. * build systems This library builds on top of the @mvc@ library, so you may want to read the documentation in the "MVC" module if you haven't already. Here is an example program to illustrate how this library works: > import Control.Applicative ((<$>), (<*>)) > import Control.Foldl (last, length) > import MVC.Updates (Updatable, on, listen, runUpdatable) > import MVC.Prelude (stdinLines, tick) > import Prelude hiding (last, length) > > data Example = Example (Maybe String) Int deriving (Show) > > debug :: Show a => String -> Updatable a -> Updatable a > debug label = listen (\x -> putStrLn (label ++ ": " ++ show x)) > > lastLine :: Updatable (Maybe String) > lastLine = debug "lastLine" (on last stdinLines) > > seconds :: Updatable Int > seconds = debug "seconds " (on length (tick 1.0)) > > example :: Updatable Example > example = debug "example " (Example <$> lastLine <*> seconds) > > main :: IO () > main = runUpdatable example First we build two simple `Updatable` values: * @lastLine@ updates every time the user enters a new line at standard input * @seconds@ increments every second Additionally, the `debug` function attaches a listener to each value that prints updates to the console. Every listener triggers once at the beginning of the program and once for each update to the attached value. Then we assemble these two `Updatable` values into a derived `Updatable` value using `Applicative` operations. This derived value updates every time one of the two original values updates: > $ ./example > lastLine: Nothing > seconds : 0 > example : Example Nothing 0 > Test > lastLine: Just "Test" > example : Example (Just "Test") 0 > seconds : 1 > example : Example (Just "Test") 1 > seconds : 2 > example : Example (Just "Test") 2 > ABC > lastLine: Just "ABC" > example : Example (Just "ABC") 2 > seconds : 3 > example : Example (Just "ABC") 3 > ... At the beginning of the program we see one debug output for each value's initialization. Afterwards, we see updates every time the user enters a line of input or one second passes. Updates are efficient. When the user enters a new line, the `Example` value reuses the cached value for seconds. Similarly, when one second passes, the `Example` reuses the cached value for the last line. The Example section at the bottom of this module contains an extended example for how to build a GTK-based spreadsheet using this library. -} module MVC.Updates ( -- * Updates -- $updates Updatable(..) , on , listen , runUpdatable , updates -- * Example -- $example -- * Re-exports -- $reexports , module Control.Foldl ) where import Control.Applicative (Applicative(pure, (<*>)), (<*), liftA2) import Control.Category (id) import Control.Concurrent.Async (withAsync) import Control.Foldl (FoldM(..), Fold(..)) import qualified Control.Foldl as L import Data.IORef (newIORef, readIORef, writeIORef) import MVC import Prelude hiding (id) {- $updates You can combine smaller updates into larger updates using `Applicative` operations: > _As :: Updatable A > _Bs :: Updatable B > > _ABs :: Updatable (A, B) > _ABs = liftA2 (,) _As _Bs @_ABs@ updates every time either @_As@ updates or @_Bs@ updates, caching and reusing values that do not update. For example, if @_As@ emits a new @A@, then @_ABs@ reuses the old value for @B@. Vice versa, if @_Bs@ emits a new @B@ then @_ABs@ reuses the old value for @A@. This caching behavior transitively works for any number of updates that you combine using `Applicative` operations. Also, the internal code is efficient and does not introduce any new threads no matter how many updates you combine. (Note: the `updates` function does introduce one additional thread) Tip: To efficiently merge a large number of updates, store them in a `Data.Sequence.Seq` and use `Data.Foldable.sequenceA` to merge them: > sequenceA :: Seq (Updatable a) -> Updatable (Seq a) -} -- | A concurrent, updatable value data Updatable a = forall e . Updatable (Managed (Controller e, FoldM IO e a)) instance Functor Updatable where fmap f (Updatable m) = Updatable (fmap (fmap (fmap f)) m) -- _Left :: Traversable' (Either a b) a _Left :: Applicative f => (a -> f a) -> (Either a b -> f (Either a b)) _Left k e = case e of Left a -> fmap Left (k a) Right b -> pure (Right b) -- _Right :: Traversable' (Either a b) b _Right :: Applicative f => (b -> f b) -> (Either a b -> f (Either a b)) _Right k e = case e of Left a -> pure (Left a) Right b -> fmap Right (k b) instance Applicative Updatable where pure a = Updatable (pure (pure (pure a))) Updatable mL <*> Updatable mR = Updatable (liftA2 f mL mR) where f (controllerL, foldL) (controllerR, foldR) = (controllerT, foldT) where foldT = L.pretraverseM _Left foldL <*> L.pretraverseM _Right foldR controllerT = fmap Left controllerL <> fmap Right controllerR -- | Create an `Updatable` value using a pure `Fold` on :: Fold e a -> Managed (Controller e) -> Updatable a on fold m = Updatable (fmap (\controller -> (controller, L.generalize fold)) m) {-# INLINABLE on #-} {-| Attach a listener that runs every time an `Updatable` value updates > -- Treating `a -> IO ()` as the `View a` `Monoid`: > > listen mempty = id > > listen (f <> g) = listen g . listen f -} listen :: (a -> IO ()) -> Updatable a -> Updatable a listen handler (Updatable m) = Updatable (fmap f m) where f (controller, FoldM step begin done) = (controller, FoldM step' begin' done) where begin' = do x <- begin b <- done x handler b return x step' x a = do x' <- step x a b <- done x' handler b return x' {-# INLINABLE listen #-} {-| Run an `Updatable` value, discarding the result Use this if you only care about running the associated listeners -} runUpdatable :: Updatable a -> IO () runUpdatable (Updatable m) = runMVC () id $ do (controller, FoldM step begin done) <- m ioref <- liftIO $ do x <- begin _ <- done x newIORef x let view = asSink $ \e -> do x <- readIORef ioref x' <- step x e _ <- done x' writeIORef ioref x' return (view, controller) {-# INLINABLE runUpdatable #-} {-| Convert an `Updatable` value to a `Managed` `Controller` that emits updates You must specify how to `Buffer` the updates -} updates :: Buffer a -> Updatable a -> Managed (Controller a) updates buffer (Updatable m) = do (controller, FoldM step begin done) <- m managed $ \k -> do (o, i, seal) <- spawn' buffer ioref <- liftIO $ do x <- begin a <- done x _ <- atomically $ send o a newIORef x let view = asSink $ \e -> do x <- readIORef ioref x' <- step x e a <- done x' _ <- atomically $ send o a writeIORef ioref x' let io = do _ <- runMVC begin id (pure (view, controller)) atomically seal withAsync io $ \_ -> k (asInput i) <* atomically seal -- $example -- -- The following example program shows how to build a spreadsheet with input and -- output cells using the @gtk@, @mvc@ and @mvc-updates@ libraries. -- -- You can find this and other examples on: -- -- -- -- The first half of the program contains all the @gtk@-specific logic. The -- key function is @spreadsheet@, which returns high-level commands to build -- multiple input and output cells. -- -- > -- This must be compiled with the `-threaded` flag -- > -- > {-# LANGUAGE TemplateHaskell #-} -- > -- > import Control.Applicative (Applicative, (<$>), (<*>)) -- > import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar) -- > import Control.Concurrent.Async (async, wait) -- > import Control.Foldl (lastDef) -- > import Graphics.UI.Gtk as GTK -- > import Lens.Family.TH (makeLenses) -- > import MVC -- > import MVC.Updates as MVC -- > -- > makeInCell :: VBox -> Updatable Double -- > makeInCell vBox = MVC.on (lastDef 0) $ managed $ \k -> do -- > (output, input) <- spawn Unbounded -- > spinButton <- spinButtonNewWithRange 0 100 1 -- > onValueSpinned spinButton $ do -- > n <- get spinButton spinButtonValue -- > _ <- atomically (send output n) -- > return () -- > boxPackStartDefaults vBox spinButton -- > widgetShowAll vBox -- > k (asInput input) -- > -- > makeOutCell :: VBox -> Managed (View Double) -- > makeOutCell vBox = liftIO $ do -- > entry <- entryNew -- > boxPackStartDefaults vBox entry -- > return $ asSink $ \n -> postGUISync $ entrySetText entry (show n) -- > -- > spreadsheet :: Managed (Updatable Double, Managed (View Double), IO ()) -- > spreadsheet = managed $ \k -> do -- > initGUI -- > window <- windowNew -- > hBox <- hBoxNew False 0 -- > vBoxL <- vBoxNew False 0 -- > vBoxR <- vBoxNew False 0 -- > set window [windowTitle := "Spreadsheet", containerChild := hBox] -- > boxPackStartDefaults hBox vBoxL -- > boxPackStartDefaults hBox vBoxR -- > -- > mvar <- newEmptyMVar -- > a <- async $ k (makeInCell vBoxL, makeOutCell vBoxR, putMVar mvar ()) -- > takeMVar mvar -- > -- > GTK.on window deleteEvent $ do -- > liftIO mainQuit -- > return False -- > widgetShowAll window -- > mainGUI -- > wait a -- -- Input cells are `Updatable` values, and output cells are `Managed` -- `View`s. Since `Updatable` values are `Applicative`s, we can combine -- input cells into a single `Updatable` value (represented by the @In@ -- type) that updates whenever any individual cell updates: -- -- > data Out = O { _o1 :: Double, _o2 :: Double, _o3 :: Double, _o4 :: Double } -- > -- > data In = I { _i1 :: Double, _i2 :: Double, _i3 :: Double, _i4 :: Double } -- > -- > makeLenses ''Out -- > o1, o2, o3, o4 :: Functor f => (Double -> f Double) -> Out -> f Out -- > -- > model :: Model () In Out -- > model = asPipe $ loop $ \(I i1 i2 i3 i4) -> do -- > return $ O (i1 + i2) (i2 * i3) (i3 - i4) (max i4 i1) -- > -- > main :: IO () -- > main = runMVC () model $ do -- > (inCell, outCell, go) <- spreadsheet -- > c <- updates Unbounded $ I <$> inCell <*> inCell <*> inCell <*> inCell -- > v <- fmap (handles o1) outCell -- > <> fmap (handles o2) outCell -- > <> fmap (handles o3) outCell -- > <> fmap (handles o4) outCell -- > liftIO go -- > return (v, c) -- > -- > -- This must be compiled with the `-threaded` flag -- -- The @model@ contains the pure fragment of our program that relates input -- cells to output cells. In this example, each output cell is a function -- of two input cells. -- -- If you compile and run the above program with the @-threaded@ flag, a -- small spread sheet window will open with input cells on the left-hand -- side and output cells on the right-hand side. Modifying any input cell -- will automatically update all output cells. {- $reexports "Control.Foldl" re-exports the `Fold` and `FoldM` types -}