diff --git a/core/Shpadoinkle/Continuation.hs b/core/Shpadoinkle/Continuation.hs index df430cd96abebd5e3946cb1a3a4856e925955e0a..b83b03f7f00e203576dfd1b50dd88e2445eb9363 100644 --- a/core/Shpadoinkle/Continuation.hs +++ b/core/Shpadoinkle/Continuation.hs @@ -4,6 +4,7 @@ {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} @@ -19,7 +20,7 @@ module Shpadoinkle.Continuation ( -- * The Continuation Type Continuation (..) , runContinuation - , done, pur, impur, kleisli, causes, causedBy, merge, contIso, before, after + , done, pur, impur, kleisli, causes, causedBy, merge, contIso, before, after, meanwhile -- * The Class , Continuous (..) -- ** Hoist @@ -74,6 +75,10 @@ import UnliftIO.Concurrent (forkIO) -- updates generated so far but allows for further state updates to be generated based on -- further monadic IO computation. -- +-- A Continuation may Fork, which causes it to split into two separately and concurrently +-- running continuations. The left hand side continuation is a continuation of whatever +-- continuation the fork occurs inside of. +-- -- The functions generating each stage of the Continuation -- are called with states which reflect the current state of the app, with all -- the pure state updating functions generated so far having been @@ -85,7 +90,7 @@ data Continuation m a = Continuation (a -> a) (a -> m (Continuation m a)) | Rollback (Continuation m a) | Merge (Continuation m a) | Pure (a -> a) - + | Fork (Continuation m a) (Continuation m a) -- | A pure state updating function can be turned into a Continuation. This function @@ -137,12 +142,19 @@ Pure f `before` Pure g = Pure (g.f) Merge f `before` g = Merge (f `before` g) Rollback f `before` g = Rollback (f `before` g) Continuation f g `before` h = Continuation f $ fmap (`before` h) . g +Fork f g `before` h = (f `before` h) `meanwhile` g +f `before` (Fork g h) = (f `before` g) `meanwhile` h after :: Applicative m => Continuation m a -> Continuation m a -> Continuation m a after = flip before +-- | Runs two continuations in parallel. +meanwhile :: Continuation m a -> Continuation m a -> Continuation m a +meanwhile = Fork + + -- | 'runContinuation' takes a 'Continuation' and a state value and runs the whole Continuation -- as if the real state was frozen at the value given to 'runContinuation'. It performs all the -- IO actions in the stages of the Continuation and returns a pure state updating function @@ -151,6 +163,8 @@ after = flip before -- territory, then you should probably be using 'writeUpdate' instead of 'runContinuation', -- because 'writeUpdate' will allow each stage of the Continuation to see any extant updates -- made to the territory after the Continuation started running. +-- +-- 'runContinuation' does not execute forks concurrently; instead, it executes them serially. {-# SPECIALIZE runContinuation :: Continuation JSM a -> a -> JSM (a -> a) #-} runContinuation :: Monad m => Continuation m a -> a -> m (a -> a) runContinuation = runContinuation' id @@ -164,6 +178,7 @@ runContinuation' f (Continuation g h) x = do runContinuation' _ (Rollback f) x = runContinuation' id f x runContinuation' f (Merge g) x = runContinuation' f g x runContinuation' f (Pure g) _ = return (g.f) +runContinuation' f (Fork g h) x = runContinuation' f (g `before` h) x -- | @f@ is a Functor to Hask from the category where the objects are @@ -179,19 +194,21 @@ instance Continuous Continuation where -- | Given a natural transformation, change a Continuation's underlying functor. {-# SPECIALIZE hoist :: (forall b. JSM b -> n b) -> Continuation JSM a -> Continuation n a #-} hoist :: Functor m => (forall b. m b -> n b) -> Continuation m a -> Continuation n a -hoist _ (Pure f) = Pure f -hoist f (Rollback r) = Rollback (hoist f r) -hoist f (Merge g) = Merge (hoist f g) +hoist _ (Pure f) = Pure f +hoist f (Rollback r) = Rollback (hoist f r) +hoist f (Merge g) = Merge (hoist f g) hoist f (Continuation g h) = Continuation g $ \x -> f $ hoist f <$> h x +hoist f (Fork g h) = Fork (hoist f g) (hoist f h) -- | Apply a lens inside a Continuation to change the Continuation's type. {-# SPECIALIZE liftC' :: (a -> b -> b) -> (b -> a) -> Continuation JSM a -> Continuation JSM b #-} liftC' :: Functor m => (a -> b -> b) -> (b -> a) -> Continuation m a -> Continuation m b -liftC' f g (Pure h) = Pure (\x -> f (h (g x)) x) -liftC' f g (Rollback r) = Rollback (liftC' f g r) -liftC' f g (Merge h) = Merge (liftC' f g h) +liftC' f g (Pure h) = Pure (\x -> f (h (g x)) x) +liftC' f g (Rollback r) = Rollback (liftC' f g r) +liftC' f g (Merge h) = Merge (liftC' f g h) liftC' f g (Continuation h i) = Continuation (\x -> f (h (g x)) x) (\x -> liftC' f g <$> i (g x)) +liftC' f g (Fork h i) = Fork (liftC' f g h) (liftC' f g i) -- | Apply a traversal inside a Continuation to change the Continuation's type. @@ -200,6 +217,7 @@ liftCMay' :: Applicative m => (a -> b -> b) -> (b -> Maybe a) -> Continuation m liftCMay' f g (Pure h) = Pure $ \x -> maybe x (flip f x . h) $ g x liftCMay' f g (Rollback r) = Rollback (liftCMay' f g r) liftCMay' f g (Merge h) = Merge (liftCMay' f g h) +liftCMay' f g (Fork h i) = Fork (liftCMay' f g h) (liftCMay' f g i) liftCMay' f g (Continuation h i) = Continuation (\x -> maybe x (flip f x . h) $ g x) ( maybe (pure done) (fmap (liftCMay' f g) . i) . g) @@ -265,9 +283,10 @@ rightC = mapC rightC' -- | Transform a Continuation to work on 'Maybe's. If it encounters 'Nothing', then it cancels itself. {-# SPECIALIZE maybeC' :: Continuation JSM a -> Continuation JSM (Maybe a) #-} maybeC' :: Applicative m => Continuation m a -> Continuation m (Maybe a) -maybeC' (Pure f) = Pure (fmap f) -maybeC' (Rollback r) = Rollback (maybeC' r) -maybeC' (Merge f) = Merge (maybeC' f) +maybeC' (Pure f) = Pure (fmap f) +maybeC' (Rollback r) = Rollback (maybeC' r) +maybeC' (Merge f) = Merge (maybeC' f) +maybeC' (Fork f g) = Fork (maybeC' f) (maybeC' g) maybeC' (Continuation f g) = Continuation (fmap f) $ \case Just x -> maybeC' <$> g x @@ -292,9 +311,10 @@ comaybe f x = fromMaybe x . f $ Just x -- instead the current value is retained. {-# SPECIALIZE comaybeC' :: Continuation JSM (Maybe a) -> Continuation JSM a #-} comaybeC' :: Functor m => Continuation m (Maybe a) -> Continuation m a -comaybeC' (Pure f) = Pure (comaybe f) -comaybeC' (Rollback r) = Rollback (comaybeC' r) -comaybeC' (Merge f) = Merge (comaybeC' f) +comaybeC' (Pure f) = Pure (comaybe f) +comaybeC' (Rollback r) = Rollback (comaybeC' r) +comaybeC' (Merge f) = Merge (comaybeC' f) +comaybeC' (Fork f g) = Fork (comaybeC' f) (comaybeC' g) comaybeC' (Continuation f g) = Continuation (comaybe f) ( fmap comaybeC' . g . Just) @@ -323,12 +343,13 @@ mapRight f (Right x) = Right (f x) -- completes than it was when the Continuation started, then the -- coproduct Continuation will have no effect on the state. {-# SPECIALIZE eitherC' :: Continuation JSM a -> Continuation JSM b -> Continuation JSM (Either a b) #-} -eitherC' :: Applicative m => Continuation m a -> Continuation m b -> Continuation m (Either a b) +eitherC' :: forall m a b. Applicative m => Continuation m a -> Continuation m b -> Continuation m (Either a b) eitherC' f g = Continuation id $ \case Left x -> case f of Pure h -> pure (Pure (mapLeft h)) Rollback r -> pure . Rollback $ eitherC' r done Merge h -> pure . Merge $ eitherC' h done + Fork h i -> pure $ Fork (eitherC' h done) (eitherC' i done) Continuation h i -> (\j -> Continuation (mapLeft h) ( const . pure $ eitherC' j (Rollback done))) <$> i x @@ -336,6 +357,7 @@ eitherC' f g = Continuation id $ \case Pure h -> pure (Pure (mapRight h)) Rollback r -> pure . Rollback $ eitherC' done r Merge h -> pure . Merge $ eitherC' done h + Fork h i -> pure $ Fork (eitherC' done h) (eitherC' done i) Continuation h i -> (\j -> Continuation (mapRight h) (const . pure $ eitherC' (Rollback done) j)) <$> i x @@ -358,6 +380,7 @@ contIso :: Functor m => (a -> b) -> (b -> a) -> Continuation m a -> Continuation contIso f g (Continuation h i) = Continuation (f.h.g) (fmap (contIso f g) . i . g) contIso f g (Rollback h) = Rollback (contIso f g h) contIso f g (Merge h) = Merge (contIso f g h) +contIso f g (Fork h i) = Fork (contIso f g h) (contIso f g i) contIso f g (Pure h) = Pure (f.h.g) @@ -368,21 +391,7 @@ contIso f g (Pure h) = Pure (f.h.g) -- all of them are done. A merge in any one of the branches will cause all of -- the changes that branch can see to be merged. instance Applicative m => Semigroup (Continuation m a) where - (Continuation f g) <> (Continuation h i) = - Continuation (f.h) (\x -> (<>) <$> g x <*> i x) - (Continuation f g) <> (Rollback h) = - Rollback (Continuation f (fmap (<> h) . g)) - (Rollback h) <> (Continuation _ g) = - Rollback (Continuation id (fmap (h <>) . g)) - (Rollback f) <> (Rollback g) = Rollback (f <> g) - (Pure f) <> (Pure g) = Pure (f.g) - (Pure f) <> (Continuation g h) = Continuation (f.g) h - (Continuation f g) <> (Pure h) = Continuation (f.h) g - (Pure f) <> (Rollback g) = Continuation f (const (pure (Rollback g))) - (Rollback f) <> (Pure _) = Rollback f - (Merge f) <> g = Merge (f <> g) - f <> (Merge g) = Merge (f <> g) - + (<>) = meanwhile -- | Since combining Continuations homogeneously is an associative operation, -- and this operation has a unit element (done), Continuations are a 'Monoid'. @@ -401,6 +410,9 @@ writeUpdate' h model f = do Merge g -> do atomically $ writeTVar model . h =<< readTVar model writeUpdate' id model (const (return g)) + Fork g j -> do + void . forkIO $ writeUpdate' id model (const (return j)) + writeUpdate' h model (const (return g)) Rollback gs -> writeUpdate' id model (const (return gs)) @@ -413,6 +425,9 @@ writeUpdate model = \case Continuation f g -> void . forkIO $ writeUpdate' f model g Pure f -> atomically (writeTVar model . f =<< readTVar model) Merge f -> writeUpdate model f + Fork g h -> + do void . forkIO $ writeUpdate model g + writeUpdate model h Rollback f -> writeUpdate model f diff --git a/examples/Clock.hs b/examples/Clock.hs new file mode 100644 index 0000000000000000000000000000000000000000..c214c40b48caa7edb9664d8ef12f4726ce23d907 --- /dev/null +++ b/examples/Clock.hs @@ -0,0 +1,43 @@ +{-# LANGUAGE OverloadedStrings #-} + + +module Main where + + +import Prelude hiding (span) + +import Control.Concurrent (threadDelay) +import Control.Monad.IO.Class (liftIO) +import Control.Monad.Trans.Class (lift) +import Data.Text (pack) +import Shpadoinkle (Html, Continuation, JSM, voidRunContinuationT, commit, merge, pur, text, newTVarIO, shpadoinkle, writeUpdate) +import Shpadoinkle.Html (div_, h2_, getBody) +import Shpadoinkle.Run (runJSorWarp) +import Shpadoinkle.Backend.ParDiff (runParDiff) + + +view :: Int -> Html m Int +view time = div_ + [ h2_ [ "Clock Example" ] + , "The current time is T+" + , text . pack $ show time + , " seconds" + ] + + +clock :: Continuation JSM Int +clock = voidRunContinuationT $ do + lift . liftIO $ threadDelay 1000000 + commit $ pur (+1) + commit $ merge clock + + +app :: JSM () +app = do + model <- newTVarIO 0 + writeUpdate model clock + shpadoinkle id runParDiff model view getBody + + +main :: IO () +main = runJSorWarp 8080 app diff --git a/examples/ClockWithAttentionModal.hs b/examples/ClockWithAttentionModal.hs new file mode 100644 index 0000000000000000000000000000000000000000..b66537bc65bcc80bd1b57100bc9f8bc13031a272 --- /dev/null +++ b/examples/ClockWithAttentionModal.hs @@ -0,0 +1,111 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} + + +module Main where + + +import Prelude hiding (div) + +import Control.Concurrent (threadDelay) +import Control.Monad.IO.Class (liftIO) +import Control.Monad.Trans.Class (lift) +import Data.Text (pack) +import GHC.Generics (Generic) +import Shpadoinkle (NFData, MonadJSM, Html, Continuation, JSM, liftC, voidRunContinuationT, commit, merge, pur, text, newTVarIO, shpadoinkle, writeUpdate) +import Shpadoinkle.Html (div_, h2_, button, getBody, onClickC) +import Shpadoinkle.Run (runJSorWarp) +import Shpadoinkle.Backend.ParDiff (runParDiff) + + +newtype Seconds = Seconds Int + deriving newtype (Eq, Ord, Show, Num) + deriving (Generic, NFData) + + +newtype Clock = Clock { unClock :: Seconds } + deriving newtype (Eq, Ord, Show, Num) + deriving (Generic, NFData) + + +newtype Idle = Idle { unIdle :: Seconds } + deriving newtype (Eq, Ord, Show, Num) + deriving Generic + +instance NFData Idle + + +data Model = Model { getClock :: Clock, getIdle :: Idle } + deriving (Eq, Show, Generic) + +instance NFData Model + + +clockC :: Functor m => Continuation m Clock -> Continuation m Model +clockC = liftC (\c (Model _ i) -> Model c i) getClock + + +idleC :: Functor m => Continuation m Idle -> Continuation m Model +idleC = liftC (\i (Model c _) -> Model c i) getIdle + + +clockView :: Clock -> Html m a +clockView (Clock time) = div_ + [ h2_ [ "Clock / Attention Example" ] + , "The current time is T+" + , text . pack $ show time + , " seconds" + ] + + +idleView :: Functor m => Html m Model +idleView = + div_ + [ h2_ [ "Clock / Attention Example" ] + , "Are you paying attention?" + , div_ [ button [ onClickC attention ] [ "Yes" ] ] + , div_ [ button [ onClickC attention ] [ "No" ] ] + ] + + +idleThreshold :: Seconds +idleThreshold = 5 + + +view :: Functor m => Model -> Html m Model +view (Model clockTime (Idle idleTime)) = + if idleTime > idleThreshold + then idleView + else clockView clockTime + + +incrementEachSecond :: Num a => MonadJSM m => Continuation m a +incrementEachSecond = voidRunContinuationT $ do + lift . liftIO $ threadDelay 1000000 + commit $ pur (+1) + commit $ merge incrementEachSecond + + +clock :: MonadJSM m => Continuation m Model +clock = clockC incrementEachSecond + + +idle :: MonadJSM m => Continuation m Model +idle = idleC incrementEachSecond + + +attention :: Functor m => Continuation m Model +attention = idleC (pur (const 0)) + + +app :: JSM () +app = do + model <- newTVarIO (Model (Clock 0) (Idle 0)) + writeUpdate model (clock <> idle) + shpadoinkle id runParDiff model view getBody + + +main :: IO () +main = runJSorWarp 8080 app diff --git a/examples/Shpadoinkle-examples.cabal b/examples/Shpadoinkle-examples.cabal index 3971d7fbaf337b8c3b712f046fcd24933aa96537..97a6bbaffd2edf064e67803a6f47f1a54241dbd6 100644 --- a/examples/Shpadoinkle-examples.cabal +++ b/examples/Shpadoinkle-examples.cabal @@ -109,6 +109,38 @@ executable calculator-ie default-language: Haskell2010 +executable clock + import: ghc-options, ghcjs-options + + main-is: Clock.hs + + hs-source-dirs: ./. + + build-depends: + Shpadoinkle + , Shpadoinkle-backend-pardiff + , Shpadoinkle-html + , base >=4.12.0 && <4.16 + , text + , transformers + + +executable clock-with-attention-modal + import: ghc-options, ghcjs-options + + main-is: ClockWithAttentionModal.hs + + hs-source-dirs: ./. + + build-depends: + Shpadoinkle + , Shpadoinkle-backend-pardiff + , Shpadoinkle-html + , base >=4.12.0 && <4.16 + , text + , transformers + + executable counter import: ghc-options, ghcjs-options