simple-actors 0.3.0 → 0.4.0
raw patch · 2 files changed
+308/−100 lines, 2 filesdep ~contravariantdep ~mtl
Dependency ranges changed: contravariant, mtl
Files
- Control/Concurrent/Actors.lhs +291/−91
- simple-actors.cabal +17/−9
Control/Concurrent/Actors.lhs view
@@ -1,4 +1,4 @@-> {-# LANGUAGE GeneralizedNewtypeDeriving, MultiParamTypeClasses #-}+> {-# LANGUAGE GeneralizedNewtypeDeriving, MultiParamTypeClasses, TypeFamilies, TypeOperators #-} This module exports a simple, idiomatic implementation of the Actor Model. @@ -42,16 +42,16 @@ > > -- operations supported by the network: > > data Operation = Insert { val :: Int } > > | Query { val :: Int-> > , sigVar :: MVar Bool }+> > , sigVar :: Mailbox Bool } > > > > insert :: Node -> Int -> IO () > > insert t = send t . Insert > > -> > -- MVar is in the 'SplitChan' class so actors can 'send' to it: > > query :: Node -> Int -> IO Bool > > query t a = do+> > -- turn an MVar into a Mailbox actors can send to with 'out' > > v <- newEmptyMVar-> > send t (Query a v)+> > send t (Query a $ out v) > > takeMVar v > > You can use the tree defined above in GHCi:@@ -76,17 +76,23 @@ > -- * Available actions > -- ** Message passing > , Mailbox()+> , out > , send , send' , (<->) > , received > , guardReceived > -- ** Spawning actors+> , Sources(), Joined+> --, (:-:)(..)+> , spawn+> -- *** Mailboxes and scoping > {- | -> The 'spawn' function will be sufficient for forking actors in most cases,-> but launching mutually-communicating actors presents a problem.+> Straightforward use of the 'spawn' function will be sufficient for+> forking actors in most cases, but launching mutually-communicating actors+> presents a problem. > > In cases where a 'Behavior' needs access to its own 'Mailbox' or that of > an actor that must be forked later, the 'MonadFix' instance should be-> used. GHC\'s \"Recursive Do\" make this especially easy:+> used. GHC\'s \"Recursive Do\" notation make this especially easy: > > > {-# LANGUAGE DoRec #-} > > beh = Receive $ do@@ -97,14 +103,11 @@ > > b3 <- spawn (senderTo b3) > > -- send initial messages to actors spawned above: > > send b3 i-> > send "first" b2+> > send b2 "first" > > yield > > -} >-> , spawn-> , spawn_-> , spawnReading > -- ** Building an actor computation > {- | > An actor computation can be halted immediately by calling 'yield',@@ -136,10 +139,10 @@ > product types. > -} > , coproductMb-> , productMb+> , contraProduct > , zipMb-> , faninMb-> , fanoutMb+> , contraFanin+> , contraFanout > > -- * Utility functions > {- | @@ -157,6 +160,7 @@ > ) where > > import Control.Monad+> import Control.Applicative > import Control.Monad.Reader(ask) > import qualified Data.Foldable as F > import Control.Monad.IO.Class@@ -168,7 +172,6 @@ > import Data.Functor.Contravariant > -- from the chan-split package > import Control.Concurrent.Chan.Split-> > -- internal: > import Control.Concurrent.Actors.Behavior @@ -178,28 +181,33 @@ TODO ----- -0.4- - allow destructuring using UndecidableInstances (see mockup) on spawn, allowing for new, awesome synchronization semantics!- - make that also work with Behaviors of arbitrary input types using new GHC generics!---Later:+0.4.1 - performance tuning / benchmarking:- + look at interface file: ghc -ddump-hi Control/Concurrent/Actors.hs -O -c- + remove current PRAGMA- - close browser and everything, do a fake quick benchmark to get clock info- - be more controlled about the source lists (do once before defaultMain), use 'evaluate'- - run with +RTS -s and make sure everything is 0- - see if case-based nil is better- - get accurate baseline comparison between actors and set- - use INLINABLE- - test again with SPECIALIZE instead- - try adding INLINE to all with higher-order args (or higher-order newtype wrappers)- and make sure our LHS looks good for inlining- - specialize `Action i (Behavior i)` or allow lots of unfolding... ? Optimize those loops, somehow. Rewrite rules?+ - first optimize TreeExample, by way of Benchmark.hs+ - criterion and profiling w/r/t lib.:+ - play with underlying Behavior Monad stack?+ - be more controlled about the source lists (do once before defaultMain), use 'evaluate'+ - run with +RTS -s and make sure everything is 0+ - see if case-based nil is better+ - try storing the same chan (observable sharing) in each node, and use for streaming + send an MVar with messages for the query operation+ - get accurate baseline comparison between actors and set+ - use INLINABLE+ - test again with SPECIALIZE instead+ - try adding INLINE to all with higher-order args (or higher-order newtype wrappers)+ and make sure our LHS looks good for inlining+ - specialize `Action i (Behavior i)` or allow lots of unfolding... ? Optimize those loops, somehow. Rewrite rules?+ - look at "let floating" and INLINEABLE to get functions with "fully-applied (syntactically) LHS"+ - split-chan ChItem in heap profile -hy - take a look at threadscope for random tree test- - look at "let floating" and INLINEABLE to get functions with "fully-applied (syntactically) LHS"+ - forkOnIO to keep communicating actors on same HEC? - compare with previous version (cp to /tmp to use previous version)+++Later:+ - make that also work with Behaviors of arbitrary input types using new GHC generics?+ - can we make joins work with arbitrary types using Generics?+ - can we support Either in Sources? - get complete code coverage into simple test module - interesting solution to exit detection: http://en.wikipedia.org/wiki/Huang%27s_algorithm@@ -220,7 +228,7 @@ - a pre-declared Mailbox for IO? Eventually:- - some sort of exception handling technique (using actors?)+ - some sort of exception handling technique a.la erlang - abilty to launch an actor that automatically "replicates" if its chan needs more consumers. This should probably be restricted to an `Action i ()` that we repeat.@@ -228,7 +236,7 @@ optimizing message flow with some algorithm? - provide an "adapter" for amazon SQS, allowing truly distributed message passing- - investigate erlang-style selective receive (using Alternative?)+ - play w/ distributed-process (cloud haskell) - consider: combining TChans, where values are popped off when available, for chan-split? - look at ways we can represent network IO as channels to interface with@@ -245,7 +253,7 @@ (maybe letting us use useful enumerators) ...also now pipes, conduits, etc. etc. - - study ambient/join/fusion calculi for clues as to where it's really at+ - study ambient/join/fusion calculi for clues to where it's really at CHAN TYPES@@ -267,17 +275,30 @@ > runMailbox :: Mailbox a -> a -> IO () > runMailbox = getOp . sender >-> mkMailbox :: InChan a -> Mailbox a-> mkMailbox = mailbox . writeChan-> > mkMessages :: OutChan a -> Messages a > mkMessages = Messages . readChan > > -- | One can 'send' a messages to a @Mailbox@ where it will be processed > -- according to an actor\'s defined 'Behavior'+> --+> -- > type Joined (Mailbox a) = a > newtype Mailbox a = Mailbox { sender :: Sender a } > deriving (Contravariant) ++Previously we were polymorphic in SplitChan in many places. Now that spawn+has polymorphic result type we simply export a function to convert from+any SplitChan type. Otherwise we'd have to provide type annotations everywhere.++I liked the previous version, since a send within an actor is semantically-+identical regardless of the channel type.++> -- | Convert the input side of a @SplitChan@ to a @Mailbox@. Useful for +> -- sending data out from an actor system via a channel created in IO.+> out :: (SplitChan i x)=> i a -> Mailbox a+> out = mailbox . writeChan++ We don't need to expose this thanks to the miracle of MonadFix and recursive do, but this can be generated via the NewSplitChan class below if the user imports the library:@@ -291,7 +312,7 @@ > writeChan = runMailbox > > instance NewSplitChan Mailbox Messages where-> newSplitChan = (mkMailbox *** mkMessages) `fmap` newSplitChan+> newSplitChan = (out *** mkMessages) `fmap` newSplitChan For Mailboxes we can define all transformations associated with Cartesian and @@ -307,17 +328,22 @@ > zipMb :: Mailbox a -> Mailbox b -> Mailbox (a,b) > zipMb m1 m2 = mailbox $ \(a,b) -> writeChan m1 a >> writeChan m2 b >-> -- | > productMb = contramap Left &&& contramap Right-> productMb :: Mailbox (Either a b) -> (Mailbox a, Mailbox b)-> productMb = contramap Left &&& contramap Right++The naming here doesn't make much sense now that these are general. Keep for +now and hope we can deprecate in favor of functionality in one of E.K.'s +libs?++> -- | > contraProduct = contramap Left &&& contramap Right+> contraProduct :: Contravariant f => f (Either a b) -> (f a, f b)+> contraProduct = contramap Left &&& contramap Right >-> -- | > faninMb f g = contramap (f ||| g)-> faninMb :: (a -> c) -> (b -> c)-> Mailbox c -> Mailbox (Either a b) -> faninMb f g = contramap (f ||| g)+> -- | > contraFanin f g = contramap (f ||| g)+> contraFanin :: Contravariant f => (b -> a) -> (c -> a) -> f a -> f (Either b c)+> contraFanin f g = contramap (f ||| g) >-> -- | > fanoutMb f g = contramap (f &&& g)-> fanoutMb :: (a -> b) -> (a -> c) -> Mailbox (b,c) -> Mailbox a-> fanoutMb f g = contramap (f &&& g)+> -- | > contraFanout f g = contramap (f &&& g)+> contraFanout :: Contravariant f=> (a -> b) -> (a -> c) -> f (b,c) -> f a+> contraFanout f g = contramap (f &&& g) @@ -329,7 +355,7 @@ to import a bunch of libraries to get basic Behavior building functionality. > infixl 3 <.|>-+> > -- | Sequence two @Behavior@s. After the first 'yield's the second takes over, > -- discarding the message the former was processing. See also the 'Monoid' > -- instance for @Behavior@.@@ -355,12 +381,12 @@ > -- | Useful to make defining a continuing Behavior more readable as a > -- \"receive block\", e.g. > ---> -- > pairUp out = Receive $ do+> -- > pairUpAndSendTo mb = Receive $ do > -- > a <- received > -- > receive $ do > -- > b <- received-> -- > send out (b,a)-> -- > return (pairUp out)+> -- > send mb (b,a)+> -- > return (pairUpAndSendTo mb) > -- > -- Defined as: > --@@ -381,18 +407,17 @@ > guardReceived :: (i -> Bool) -> Action i i > guardReceived p = ask >>= \i-> guard (p i) >> return i -> -- | Send a message asynchronously. This can be used to send messages to other-> -- Actors via a 'Mailbox', or used as a means of output from the Actor system-> -- to IO since the function is polymorphic.+> -- | Send a message asynchronously to an actor receiving from Mailbox. See+> -- also 'out' for converting other types of chans to 'Mailbox'. > -- > -- > send b = liftIO . writeChan b-> send :: (MonadIO m, SplitChan c x)=> c a -> a -> m ()+> send :: (MonadIO m)=> Mailbox a -> a -> m () > send b = liftIO . writeChan b > -- | A strict 'send': > -- > -- > send' b a = a `seq` send b a-> send' :: (MonadIO m, SplitChan c x)=> c a -> a -> m ()+> send' :: (MonadIO m)=> Mailbox a -> a -> m () > send' b a = a `seq` send b a > infixr 1 <->@@ -402,7 +427,7 @@ > -- e.g. > -- > -- > do mb <- 0 <-> spawn foo-> (<->) :: (MonadIO m, SplitChan c x)=> a -> m (c a) -> m (c a)+> (<->) :: (MonadIO m)=> a -> m (Mailbox a) -> m (Mailbox a) > a <-> mmb = mmb >>= \mb-> send mb a >> return mb @@ -411,16 +436,213 @@ FORKING AND RUNNING ACTORS: =========================== +The strict Actor Model is limited in expressiveness, in that it doesn't allow+for a method of synchronization, e.g. we cannot have an actor that pairs up+incoming messages from two different channels. I think this leads to nonsense+like "selective receive" in Erlang (disclaimer: IANA erlang-xpert). -> -- | Like 'spawn' but allows one to specify explicitly the channel from which-> -- an actor should take its input. Useful for extending the library to work-> -- over other channels.-> spawnReading :: (MonadIO m, SplitChan x c)=> c i -> Behavior i -> m ()-> spawnReading str = liftIO . void . forkIO . actorRunner -> where actorRunner b =-> readChan str >>= runBehaviorStep b >>= F.mapM_ actorRunner+I've realized that I can keep all the nice semantics of actors (i.e. this+change doesn't affect Behaviors) , while supporting synchronization and+simplifying the API all at the same time! This method is inspired by the "join+calculus", and I'm sure this isn't a new idea. +To support this elegantly in the API, we define a class with associated type,+and make 'spawn' the method. This allows the pattern of joins to be determined+polymorphically based on users' pattern match! ++ NOTE: My original goal was to use GHC.Generic to support arbitrary joins on+ any Generic a=> Behavior a ...but it wasn't coming together. Let me know+ if you can figure it out.+++> -- | We extend the actor model to support joining (or synchronizing) multiple+> -- 'Mailbox'es to a single 'Behavior' input type, using a new class with an+> -- associated type. Functionality is best explained by example:+> --+> -- Spawn an actor returning it's 'Mailbox', and send it its first message:+> -- +> -- > sumTuple :: Behavior (Int, Int)+> -- >+> -- > do b <- spawn sumTuple+> -- > send b (4, 1) +> -- > ...+> --+> -- But now we would like our @sumTuple@ actor to receive each number from a different +> -- concurrent actor:+> --+> -- > do (b1, b2) <- spawn sumTuple+> -- > b3 <- spawn (multipliesBy2AndSendsTo b1)+> -- > send b3 2+> -- > send b2 1+> -- > ...+> --+> -- Lastly spawn an actor that starts immediately on an infinite supply of @()@s,+> -- and supplies an endless stream of @Int@s to @sumTuple@+> --+> -- > do (b1, b2) <- spawn sumTuple+> -- > () <- spawn (sendsIntsTo b2)+> -- > send b1 4+> -- > ...+> class Sources s where+> type Joined s+> newJoinedChan :: IO (s, Messages (Joined s)) -- private+++Spawn uses un-exported newJoinedChan where we used newSplitChan previously:++> -- | Fork an actor performing the specified 'Behavior'. /N.B./ an actor+> -- begins execution of its 'headBehavior' only after a message becomes+> -- available to process; for sending an initial message to an actor right+> -- after 'spawn'ing it, ('<|>') can be convenient.+> spawn :: (MonadIO m, Sources s)=> Behavior (Joined s) -> m s+> spawn b = liftIO $ do+> (srcs, msgs) <- newJoinedChan+> let runner b' = readChan msgs >>= runBehaviorStep b' >>= F.mapM_ runner+> void $ forkIO (runner b)+> return srcs+++...and our instance for Mailbox completes previous simple spawn functionality:++> instance Sources (Mailbox a) where+> type Joined (Mailbox a) = a+> newJoinedChan = newSplitChan+++By adding an instance for (,) synchronization and wonderful new things become+possible!++> instance (Sources a, Sources b)=> Sources (a,b) where+> type Joined (a,b) = (Joined a, Joined b)+> newJoinedChan = do+> (sa, ma) <- newJoinedChan+> (sb, mb) <- newJoinedChan+> let m' = Messages $ liftM2 (,) (readMsg ma) (readMsg mb)+> return ((sa,sb), m')+++We'll add instances up to 7-tuples, since that seems to be standard, but people+can use nested tuples:++> instance (Sources a, Sources b, Sources c, Sources d, Sources e, Sources f, Sources g)=> Sources (a,b,c,d,e,f,g) where+> type Joined (a,b,c,d,e,f,g) = (Joined a, Joined b,Joined c,Joined d,Joined e,Joined f,Joined g)+> newJoinedChan = do+> (sa, ma) <- newJoinedChan+> (sb, mb) <- newJoinedChan+> (sc, mc) <- newJoinedChan+> (sd, md) <- newJoinedChan+> (se, me) <- newJoinedChan+> (sf, mf) <- newJoinedChan+> (sg, mg) <- newJoinedChan+> let m' = Messages $ (,,,,,,) <$> readMsg ma <*> readMsg mb <*> readMsg mc <*> readMsg md <*> readMsg me <*> readMsg mf <*> readMsg mg +> return ((sa,sb,sc,sd,se,sf,sg), m')+>+> instance (Sources a, Sources b, Sources c, Sources d, Sources e, Sources f)=> Sources (a,b,c,d,e,f) where+> type Joined (a,b,c,d,e,f) = (Joined a, Joined b,Joined c,Joined d,Joined e,Joined f)+> newJoinedChan = do+> (sa, ma) <- newJoinedChan+> (sb, mb) <- newJoinedChan+> (sc, mc) <- newJoinedChan+> (sd, md) <- newJoinedChan+> (se, me) <- newJoinedChan+> (sf, mf) <- newJoinedChan+> let m' = Messages $ (,,,,,) <$> readMsg ma <*> readMsg mb <*> readMsg mc <*> readMsg md <*> readMsg me <*> readMsg mf+> return ((sa,sb,sc,sd,se,sf), m')+>+> instance (Sources a, Sources b, Sources c, Sources d, Sources e)=> Sources (a,b,c,d,e) where+> type Joined (a,b,c,d,e) = (Joined a, Joined b,Joined c,Joined d,Joined e)+> newJoinedChan = do+> (sa, ma) <- newJoinedChan+> (sb, mb) <- newJoinedChan+> (sc, mc) <- newJoinedChan+> (sd, md) <- newJoinedChan+> (se, me) <- newJoinedChan+> let m' = Messages $ (,,,,) <$> readMsg ma <*> readMsg mb <*> readMsg mc <*> readMsg md <*> readMsg me+> return ((sa,sb,sc,sd,se), m')+>+> instance (Sources a, Sources b, Sources c, Sources d)=> Sources (a,b,c,d) where+> type Joined (a,b,c,d) = (Joined a, Joined b,Joined c,Joined d)+> newJoinedChan = do+> (sa, ma) <- newJoinedChan+> (sb, mb) <- newJoinedChan+> (sc, mc) <- newJoinedChan+> (sd, md) <- newJoinedChan+> let m' = Messages $ (,,,) <$> readMsg ma <*> readMsg mb <*> readMsg mc <*> readMsg md +> return ((sa,sb,sc,sd), m')+>+> instance (Sources a, Sources b, Sources c)=> Sources (a,b,c) where+> type Joined (a,b,c) = (Joined a, Joined b,Joined c)+> newJoinedChan = do+> (sa, ma) <- newJoinedChan+> (sb, mb) <- newJoinedChan+> (sc, mc) <- newJoinedChan+> let m' = Messages $ (,,) <$> readMsg ma <*> readMsg mb <*> readMsg mc +> return ((sa,sb,sc), m')+++I give up for now on defining an instance for sums. This probably requires a+different formulation for class++ ...and we also support Either as a source, since this is the only way to get a joined+ product of sums; otherwise users could just use 'contraProduct', a pure operation.++ > -- | > type Joined (a :-: b) = Either (Joined a) (Joined b)+ > --+ > -- A product of 'Sources' corresponding to a @Behavior (Either a b)@. Allows+ > -- 'spawn'-ing a @Behavior@ which receives a sum of perhaps-'Joined' products.+ > --+ > -- See also: 'contraProduct'+ > data a :-: b = (:-:) { sourceLeft :: a+ > , sourceRight :: b }+ >+ > instance (Sources a, Sources b)=> Sources (a :-: b) where+ > type Joined (a :-: b) = Either (Joined a) (Joined b)+ > --newJoinedChan :: IO (a :-: b, Messages (Either (Joined a) (Joined b)))+ > newJoinedChan = do+ > (src, msgs) <- newSplitChan+ > let (s1, s2) = contraProduct src+ > return (decompose s1 :-: decompose s2, msgs)++ class Sources s where+ type Joined s :: *+ newJoinedChan :: IO (s, Messages (Joined s))+ decomp :: Mailbox (a,b) -> (Mailbox a, Mailbox b)+ decomp :: Mailbox a -> Mailbox a+ decomp :: Mailbox (Either a b) -> (Mailbox a :-: Mailbox b)+++We can subsume the old 'spawn_' functionality in our class as well, and imagine+returning an infinite source of ()s:++> -- | > type Joined () = ()+> --+> -- Represents an endless supply of @()@s. Allows 'spawn'-ing+> -- a @Behavior ()@ that starts immediately and loops until it 'yield'-s, e.g.+> -- +> -- > do () <- spawn startsImmediately -- :: Behavior ()+> instance Sources () where+> type Joined () = ()+> newJoinedChan = +> return ((), Messages $ return ())++Replace polymorphic craziness with old spawn_ function, when we can:++> {-# RULES "spawn_" spawn = spawn_ #-}+> spawn_ :: (MonadIO m)=> Behavior () -> m ()+> spawn_ = liftIO . void . forkIO . runBehavior_+++ NOTE: spawnReading removed in 0.4, since it was unused (by me), exposed+ confusing implementation details, supports e.g. launching an actor on a+ bounded channel which violates the Model, and doesn't provide an effective+ way to do much cool stuff like reading from a network socket.++ Instead I guess we should expose enough internals in a separate module to+ support future cool stuff.+++ RUNNING ACTORS -------------- @@ -429,7 +651,7 @@ > -- | Run a @Behavior ()@ in the main thread, returning when the computation > -- exits. > runBehavior_ :: Behavior () -> IO ()-> runBehavior_ b = runBehavior b [(),()..]+> runBehavior_ b = runBehavior b $ repeat () > > -- | run a 'Behavior' in the IO monad, taking its \"messages\" from the list. > runBehavior :: Behavior a -> [a] -> IO ()@@ -438,29 +660,7 @@ -FORKING ACTORS--------------- -> -- | Fork an actor performing the specified 'Behavior'. /N.B./ an actor -> -- begins execution of its 'headBehavior' only after a message has been -> -- received; for sending an initial message to an actor right after 'spawn'ing-> -- it, ('<|>') can be convenient.-> ---> -- See also 'spawn_'.-> spawn :: (MonadIO m)=> Behavior i -> m (Mailbox i)-> spawn b = do-> (m,s) <- liftIO newSplitChan-> spawnReading s b-> return m->-> -- | Fork a looping computation which starts immediately. Equivalent to-> -- launching a @Behavior ()@ and another 'Behavior' that sends an infinite stream of-> -- ()s to the former\'s 'Mailbox'.-> spawn_ :: (MonadIO m)=> Behavior () -> m ()-> spawn_ = liftIO . void . forkIO . runBehavior_ --- USEFUL GENERAL BEHAVIORS ======================== @@ -493,7 +693,7 @@ > -- signalling the end of some other 'Behavior'. > -- > -- > signalB c = Receive (send c () >> yield)-> signalB :: (SplitChan c x)=> c () -> Behavior i+> signalB :: Mailbox () -> Behavior i > signalB c = Receive (send c () >> yield) > -- | A @Behavior@ that discard its first input, returning the passed Behavior
simple-actors.cabal view
@@ -7,12 +7,12 @@ -- The package version. See the Haskell package versioning policy -- (http://www.haskell.org/haskellwiki/Package_versioning_policy) for -- standards guiding when and how versions should be incremented.-Version: 0.3.0+Version: 0.4.0 -- A short (one-line) description of the package. Synopsis: A library for more structured concurrent programming, based on the Actor Model-Homepage: http://coder.bsimmons.name/blog/2011/05/simple-actors-a-simple-actor-model-concurrency-library/+Homepage: http://brandon.si/code/simple-actors-0-1-0-released/ -- A longer description of the package Description: simple-actors is an EDSL-style library for writing@@ -21,15 +21,23 @@ single input value, perform some 'Action's, and return the Behavior to process the next input message it receives. .- /CHANGES/ 0.2.1 - 0.3:+ We also extend the strict actor model in various elegant ways.+ See source for this type of discussion. .- - added (@\<-\>@) convenience operator, and strict @send'@+ /CHANGES/: 0.3.0 - 0.4 .- - add composition\/transformation functions for Mailboxes+ - extend 'Control.Concurrent.Actors.spawn' to transparently support "joined+ mailboxes", i.e. join-patterns, supporting synchronization between actors.+ 'Control.Concurrent.Actors.spawn' becomes polymorphic in new 'Control.Concurrent.Actors.Sources' class .- - drop support for older base and transformers, no more CPP+ - old @spawn_@ subsumed by polymorphic @spawn@, with @()@ .- - depend on chan-split 0.4+ - @send@ and other send-like functions restricted to Mailboxes, use 'out' to convert arbitrary chans to Mailboxes+ .+ - productMb, faninMb, and fanoutMb renamed 'Control.Concurrent.Actors.contraProduct', 'Control.Concurrent.Actors.contraFanin', and 'Control.Concurrent.Actors.contraFanout', and given more general types+ .+ - old @spawnReading@ removed+ . -- The license under which the package is released.@@ -73,9 +81,9 @@ -- Packages needed in order to build this package. Build-depends: base >= 4.3 && < 5 , chan-split >= 0.4.0- , mtl >= 2+ , mtl >= 2.1.1 , transformers >= 0.3- , contravariant+ , contravariant >= 0.2.0.1 ghc-options: -Wall