{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} {-# OPTIONS_GHC -Wall #-} -- | Effectful, profunctor boxes designed for concurrency. -- -- This library follows the ideas and code from [pipes-concurrency](https://hackage.haskell.org/package/pipes-concurrency) and [mvc](https://hackage.haskell.org/package/mvc) but with some polymorphic tweaks and definitively more pretentious names. module Box ( -- $setup -- $continuations -- $boxes -- $commit -- $emit -- $state -- $finite module Box.Box, module Box.Committer, module Box.Connectors, module Box.Cont, module Box.Emitter, module Box.IO, module Box.Queue, module Box.Time, ) where import Box.Box import Box.Committer import Box.Connectors import Box.Cont import Box.Emitter import Box.IO import Box.Queue import Box.Time {- $setup >>> :set -XOverloadedStrings >>> :set -XGADTs >>> :set -XNoImplicitPrelude >>> :set -XFlexibleContexts >>> import NumHask.Prelude >>> import qualified Prelude as P >>> import Data.Functor.Contravariant >>> import Box >>> import Control.Monad.Conc.Class as C >>> import Control.Lens -} {- $continuations Continuations are very common in the API with 'Cont' as an inhouse type. >>> :t fromListE [1..3::Int] fromListE [1..3::Int] :: MonadConc m => Cont m (Emitter m Int) The applicative is usually the easiest way to think about and combine continuations with their unadorned counterparts. >>> let box' = Box <$> pure toStdout <*> fromListE ["a", "b" :: Text] >>> :t box' box' :: Cont IO (Box IO Text Text) -} {- $boxes The two basic ways of connecting up a box are related as follows: > glue c e == glueb (Box c e) > glueb == fuse (pure . pure) >>> fromToList_ [1..3] glueb [1,2,3] >>> fromToList_ [1..3] (fuse (pure . pure)) [1,2,3] 1. glue: direct fusion of committer and emitter >>> runCont $ glue <$> pure toStdout <*> fromListE (show <$> [1..3]) 1 2 3 Variations to the above code include: Use of continuation applicative operators: - the '(<*.>)' operator is short hand for runCont $ xyz '(<*>)' zy. - the '(<$.>)' operator is short hand for runCont $ xyz '(<$>)' zy. > glue <$> pure toStdout <*.> fromListE (show <$> [1..3]) > glue toStdout <$.> fromListE (show <$> [1..3]) Changing the type in the Emitter (The double fmap is cutting through the Cont and Emitter layers): > glue toStdout <$.> fmap (fmap show) (fromListE [1..3]) Changing the type in the committer (which is Contrvariant so needs to be a contramap): > glue (contramap show toStdout) <$.> fromListE [1..3] Using the box version of glue: > glueb <$.> (Box <$> pure toStdout <*> (fmap show <$> fromListE [1..3])) 2. fusion of a box, with an (a -> m (Maybe b)) function to allow for mapping, filtering and simple effects. >>> let box' = Box <$> pure toStdout <*> fromListE (show <$> [1..3]) >>> fuse (\a -> bool (pure $ Just $ "echo: " <> a) (pure Nothing) (a==("2"::Text))) <$.> box' echo: 1 echo: 3 -} {- $commit >>> commit toStdout "I'm committed!" I'm committed! True Use mapC to modify a Committer and introduce effects. >>> let c = mapC (\a -> if a==2 then (sleep 0.1 >> putStrLn "stole a 2!" >> sleep 0.1 >> pure (Nothing)) else (pure (Just a))) (contramap (show :: Int -> Text) toStdout) >>> glueb <$.> (Box <$> pure c <*> fromListE [1..3]) 1 stole a 2! 3 The monoid instance of Committer sends each commit to both mappended committers. Because effects are also mappended together, the committed result is not always what is expected. >>> let cFast = mapC (\b -> pure (Just b)) . contramap ("fast: " <>) $ toStdout >>> let cSlow = mapC (\b -> sleep 0.1 >> pure (Just b)) . contramap ("slow: " <>) $ toStdout >>> (glueb <$.> (Box <$> pure (cFast <> cSlow) <*> fromListE (show <$> [1..3]))) <* sleep 1 fast: 1 slow: 1 fast: 2 slow: 2 fast: 3 slow: 3 To approximate what is intuitively expected, use 'concurrentC'. >>> runCont $ (fromList_ (show <$> [1..3]) <$> (concurrentC cFast cSlow)) <> pure (sleep 1) fast: 1 fast: 2 fast: 3 slow: 1 slow: 2 slow: 3 This is all non-deterministic, hence the necessity for messy delays and heuristic avoidance of console races. -} {- $emit >>> ("I'm emitted!" :: Text) & Just & pure & Emitter & emit >>= print Just "I'm emitted!" >>> with (fromListE [1]) (\e' -> (emit e' & fmap show :: IO Text) >>= putStrLn & replicate 3 & sequence_) Just 1 Nothing Nothing >>> toListE <$.> (fromListE [1..3]) [1,2,3] The monoid instance is left-biased. >>> toListE <$.> (fromListE [1..3] <> fromListE [7..9]) [1,2,3,7,8,9] Use concurrentE to get some nondeterministic balance. > let es = (join $ concurrentE <$> (fromListE [1..3]) <*> (fromListE [7..9])) > glue (contramap show toStdout) <$.> es 1 2 7 3 8 9 -} {- $state State committers and emitters are related as follows: >>> runIdentity $ fmap (reverse . fst) $ flip execStateT ([],[1..4]) $ glue (hoist (zoom _1) stateC) (hoist (zoom _2) stateE) [1,2,3,4] For some reason, related to a lack of an MFunctor instance for Cont, but exactly not yet categorically pinned to a wall, the following compiles but is wrong. >>> flip runStateT [] $ runCont $ glue <$> pure stateC <*> fromListE [1..4] ((),[]) -} {- $finite Most committers and emitters will run forever until: - the glued or fused other-side returns. - the Transducer, stream or monadic action returns. Finite ends (collective noun for emitters and committers) can be created with 'sink' and 'source' eg >>> glue <$> contramap (show :: Int -> Text) <$> (sink 5 putStrLn) <*.> fromListE [1..] 1 2 3 4 5 Two infinite ends will tend to run infinitely. > glue <$> pure (contramap show toStdout) <*.> fromListE [1..] 1 2 ... 💁 ∞ Use glueN to create a finite computation. >>> glueN 4 <$> pure (contramap show toStdout) <*.> fromListE [1..] 1 2 3 4 -}