diff --git a/cabal.project b/cabal.project index 29a1c6a77775a0ead8e28bcd88233948259ff897..a6a761b7c09377c8109b1dcb2955683148037e1e 100644 --- a/cabal.project +++ b/cabal.project @@ -10,6 +10,7 @@ packages: core , marketing , html , router + , streaming , widgets , examples diff --git a/core/Shpadoinkle/Continuation.hs b/core/Shpadoinkle/Continuation.hs index 175f5a736ba027f9c853a2064842e4e379e4fd48..7b81885ed09e95358dc1e4a5a6bd03d84937ac53 100644 --- a/core/Shpadoinkle/Continuation.hs +++ b/core/Shpadoinkle/Continuation.hs @@ -17,7 +17,7 @@ module Shpadoinkle.Continuation ( -- * The Continuation Type Continuation (..) , runContinuation - , done, pur, impur, kleisli, causes, contIso + , done, pur, impur, kleisli, causes, causedBy, merge, contIso -- * The Class , Continuous (..) -- ** Hoist @@ -72,6 +72,7 @@ import UnliftIO.Concurrent (forkIO) -- finishes and they are all done atomically together. data Continuation m a = Continuation (a -> a, a -> m (Continuation m a)) | Rollback (Continuation m a) + | Merge (Continuation m a) | Pure (a -> a) @@ -107,6 +108,15 @@ causes :: Monad m => m () -> Continuation m a causes m = impur (m >> return id) +causedBy :: m (Continuation m a) -> Continuation m a +causedBy = Continuation . (id,) . const + + +-- | A continuation can be forced to write its changes midflight. +merge :: Continuation m a -> Continuation m a +merge = Merge + + -- | '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 @@ -124,6 +134,7 @@ runContinuation' f (Continuation (g, h)) x = do i <- h (f x) runContinuation' (g.f) i x runContinuation' _ (Rollback f) x = runContinuation' id f x +runContinuation' f (Merge g) x = runContinuation' f g x runContinuation' f (Pure g) _ = return (g.f) @@ -141,13 +152,15 @@ instance Continuous Continuation where 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 f (Continuation (g, h)) = Continuation . (g,) $ \x -> f $ hoist f <$> h x -- | Apply a lens inside a Continuation to change the Continuation's type. 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 (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)) @@ -155,6 +168,7 @@ liftC' f g (Continuation (h, i)) = Continuation (\x -> f (h (g x)) x, \x -> lift liftCMay' :: Applicative m => (a -> b -> b) -> (b -> Maybe a) -> Continuation m a -> Continuation m b 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 (Continuation (h, i)) = Continuation (\x -> maybe x (flip f x . h) $ g x, maybe (pure done) (fmap (liftCMay' f g) . i) . g) @@ -208,8 +222,9 @@ rightC = mapC rightC' -- | Transform a Continuation to work on 'Maybe's. If it encounters 'Nothing', then it cancels itself. maybeC' :: Applicative m => Continuation m a -> Continuation m (Maybe a) -maybeC' (Pure f) = Pure (fmap f) -maybeC' (Rollback r) = Rollback (maybeC' r) +maybeC' (Pure f) = Pure (fmap f) +maybeC' (Rollback r) = Rollback (maybeC' r) +maybeC' (Merge f) = Merge (maybeC' f) maybeC' (Continuation (f, g)) = Continuation . (fmap f,) $ \case Just x -> maybeC' <$> g x @@ -232,8 +247,9 @@ comaybe f x = fromMaybe x . f $ Just x -- when the input Continuation would replace the current value with 'Nothing', -- instead the current value is retained. comaybeC' :: Functor m => Continuation m (Maybe a) -> Continuation m a -comaybeC' (Pure f) = Pure (comaybe f) -comaybeC' (Rollback r) = Rollback (comaybeC' r) +comaybeC' (Pure f) = Pure (comaybe f) +comaybeC' (Rollback r) = Rollback (comaybeC' r) +comaybeC' (Merge f) = Merge (comaybeC' f) comaybeC' (Continuation (f,g)) = Continuation (comaybe f, fmap comaybeC' . g . Just) @@ -265,12 +281,14 @@ eitherC' f g = Continuation . (id,) $ \case Left x -> case f of Pure h -> return (Pure (mapLeft h)) Rollback r -> return . Rollback $ eitherC' r done + Merge h -> return . Merge $ eitherC' h done Continuation (h, i) -> do j <- i x return $ Continuation (mapLeft h, const . return $ eitherC' j (Rollback done)) Right x -> case g of Pure h -> return (Pure (mapRight h)) Rollback r -> return . Rollback $ eitherC' done r + Merge h -> return . Merge $ eitherC' done h Continuation (h, i) -> do j <- i x return $ Continuation (mapRight h, const . return $ eitherC' (Rollback done) j) @@ -290,7 +308,8 @@ eitherC _ r (Right x) = mapC (eitherC' (pur id)) (r x) contIso :: Functor m => (a -> b) -> (b -> a) -> Continuation m a -> Continuation m b 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 (Pure h) = Pure (f.h.g) +contIso f g (Merge h) = Merge (contIso f g h) +contIso f g (Pure h) = Pure (f.h.g) -- | @Continuation m@ is a Functor in the EndoIso category (where the objects @@ -303,8 +322,9 @@ instance Applicative m => F.Functor EndoIso EndoIso (Continuation m) where -- | You can combine multiple Continuations homogeneously using the 'Monoid' typeclass -- instance. The resulting Continuation will execute all the subcontinuations in parallel, -- allowing them to see each other's state updates and roll back each other's updates, --- applying all of the updates generated by all the subcontinuations atomically once --- all of them are done. +-- applying all of the unmerged updates generated by all the subcontinuations atomically once +-- 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 Monad m => Semigroup (Continuation m a) where (Continuation (f, g)) <> (Continuation (h, i)) = Continuation (f.h, \x -> liftM2 (<>) (g x) (i x)) @@ -318,6 +338,8 @@ instance Monad m => Semigroup (Continuation m a) where (Continuation (f,g)) <> (Pure h) = Continuation (f.h,g) (Pure f) <> (Rollback g) = Continuation (f, const (return (Rollback g))) (Rollback f) <> (Pure _) = Rollback f + (Merge f) <> g = Merge (f <> g) + f <> (Merge g) = Merge (f <> g) -- | Since combining Continuations homogeneously is an associative operation, @@ -333,6 +355,9 @@ writeUpdate' h model f = do case m of Continuation (g,gs) -> writeUpdate' (g.h) model gs Pure g -> atomically (writeTVar model . g . h =<< readTVar model) + Merge g -> do + atomically $ writeTVar model . h =<< readTVar model + writeUpdate' id model (const (return g)) Rollback gs -> writeUpdate' id model (const (return gs)) @@ -343,6 +368,7 @@ writeUpdate :: MonadUnliftIO m => TVar a -> Continuation m a -> m () 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 Rollback f -> writeUpdate model f diff --git a/default.nix b/default.nix index 710e36f07a2a36e9e7c7737ad68bbd324943e29c..fb7bec0d2fd59bd6021bef3d07c35d25163cff82 100644 --- a/default.nix +++ b/default.nix @@ -37,6 +37,7 @@ in Shpadoinkle-lens Shpadoinkle-html Shpadoinkle-router + Shpadoinkle-streaming Shpadoinkle-widgets Shpadoinkle-isreal diff --git a/examples/Streaming.hs b/examples/Streaming.hs new file mode 100644 index 0000000000000000000000000000000000000000..f24a68df4eda48947ebcfc06e79dd101543e90a7 --- /dev/null +++ b/examples/Streaming.hs @@ -0,0 +1,50 @@ +{-# LANGUAGE ExtendedDefaultRules #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PackageImports #-} + + +module Main where + + +import Prelude hiding (div) + +import Control.Concurrent (threadDelay) +import Control.Monad.IO.Class (MonadIO (liftIO)) +import Data.Text (Text, pack) +import Shpadoinkle (Html, liftC) +import Shpadoinkle.Backend.ParDiff (runParDiff) +import Shpadoinkle.Html (button, div, getBody, onClickC, + text) +import Shpadoinkle.Run (runJSorWarp, simple) +import Shpadoinkle.Streaming (consumeStream) +import "streaming" Streaming (Of, Stream) +import Streaming.Prelude (repeatM) + +default (Text) + + +exampleStream :: MonadIO m => Stream (Of Int) m () +exampleStream = repeatM $ do + liftIO $ threadDelay 1000000 + return 1 + + +newtype Model = Model { streamContents :: [Int] } + deriving (Eq, Show) + + +view :: MonadIO m => Model -> Html m Model +view (Model ns) = + div + [] + [ text (pack (show ns)) + , liftC (\c m -> m { streamContents = c }) streamContents $ + button + [ onClickC (consumeStream exampleStream (return . (:))) ] + [ text "Go" ] + ] + + +main :: IO () +main = runJSorWarp 8080 $ + simple runParDiff (Model []) view getBody diff --git a/examples/package.yaml b/examples/package.yaml index 8268422707c8e2c14aa1b5e33663652c69948b92..543d2d7349a6beaa8773c2006b9bc17b9ab47979 100644 --- a/examples/package.yaml +++ b/examples/package.yaml @@ -289,6 +289,19 @@ executables: - Shpadoinkle-backend-pardiff - Shpadoinkle-lens + streaming: + main: Streaming.hs + other-modules: [] + source-dirs: . + dependencies: + - streaming + - text + + - Shpadoinkle + - Shpadoinkle-streaming + - Shpadoinkle-html + - Shpadoinkle-backend-pardiff + git: https://gitlab.com/fresheyeball/Shpadoinkle.git diff --git a/nix/hpackall.sh b/nix/hpackall.sh index 560c5f7745ae3ec5b7489f50192c6ec8c2586d17..906451f18aadb9ba2393b4c7b06c0897cbca22c1 100755 --- a/nix/hpackall.sh +++ b/nix/hpackall.sh @@ -16,3 +16,4 @@ p widgets p examples p tests p isreal +p streaming diff --git a/nix/overlay-shpadoinkle.nix b/nix/overlay-shpadoinkle.nix index 8b82ff13b0c0da9c747856ab12e31bcfcfc63ac2..06f0ef9dd1a2fcc9a74059915869b50a065918ec 100644 --- a/nix/overlay-shpadoinkle.nix +++ b/nix/overlay-shpadoinkle.nix @@ -160,6 +160,7 @@ in { Shpadoinkle-marketing = call "Shpadoinkle-marketing" ../marketing; Shpadoinkle-html = call "Shpadoinkle-html" ../html; Shpadoinkle-router = call "Shpadoinkle-router" ../router; + Shpadoinkle-streaming = call "Shpadoinkle-streaming" ../streaming; Shpadoinkle-widgets = addTest (call "Shpadoinkle-widgets" ../widgets) hpkgs; Shpadoinkle-tests = super.haskell.packages.${compiler}.callCabal2nix "tests" (gitignore ../tests) {}; diff --git a/streaming/LICENSE b/streaming/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..e0066cfb7cd61f2d21a37d8feece3358a5fd8129 --- /dev/null +++ b/streaming/LICENSE @@ -0,0 +1,27 @@ +Shpadoinkle Streaming aka S11 Streaming +Copyright © 2021 Morgan Thomas +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Shpadoinkle nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/streaming/Setup.hs b/streaming/Setup.hs new file mode 100644 index 0000000000000000000000000000000000000000..44671092b28be990b6340bb828923fe3a19ae773 --- /dev/null +++ b/streaming/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/streaming/Shpadoinkle/Streaming.hs b/streaming/Shpadoinkle/Streaming.hs new file mode 100644 index 0000000000000000000000000000000000000000..0e74491494198a40ae18ac065974fda5c5e51a53 --- /dev/null +++ b/streaming/Shpadoinkle/Streaming.hs @@ -0,0 +1,28 @@ +{-# LANGUAGE ScopedTypeVariables #-} + + +module Shpadoinkle.Streaming + ( consumeStream + ) where + + +import Shpadoinkle hiding (h) + +import Data.Functor.Of (Of ((:>))) +import Streaming (Stream) +import Streaming.Internal (destroy) + + +consumeStream :: forall m a b. Monad m => Stream (Of a) m () -> (a -> m (b -> b)) -> Continuation m b +consumeStream stream f = destroy stream g h j + where + g :: Of a (Continuation m b) -> Continuation m b + g (a :> k) = voidRunContinuationT $ do + commit (impur (f a)) + commit (merge k) + + h :: m (Continuation m b) -> Continuation m b + h = causedBy + + j :: () -> Continuation m b + j = const done diff --git a/streaming/default.nix b/streaming/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..c7b5bb88042376313da290a47f488be514ca4239 --- /dev/null +++ b/streaming/default.nix @@ -0,0 +1,4 @@ +import ../default.nix { pack = "Shpadoinkle-streaming"; } + + + diff --git a/streaming/package.yaml b/streaming/package.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c158cdd5c455210769f5c9448beb8f9dc04000b7 --- /dev/null +++ b/streaming/package.yaml @@ -0,0 +1,34 @@ +name: Shpadoinkle-streaming +license: BSD3 +license-file: LICENSE +version: 0.0.0.1 +author: Morgan Thomas +maintainer: morgan.a.s.thomas@gmail.com +category: Web +build-type: Simple +synopsis: Integration of the streaming library with Shpadoinkle continuations. +description: + Integration of the streaming library with Shpadoinkle continuations. + + +ghc-options: + - -Wall + - -Wcompat + - -fwarn-redundant-constraints + - -fwarn-tabs + - -fwarn-incomplete-record-updates + - -fwarn-identities + + +dependencies: + - base >= 4.12.0 && < 4.16 + - lens + - streaming >= 0.2 && < 0.3 + - text + + - Shpadoinkle + + +library: + exposed-modules: Shpadoinkle.Streaming + source-dirs: .