Giter VIP home page Giter VIP logo

fused-effects's Introduction

A fast, flexible, fused effect system for Haskell

Build Status hackage

Overview

fused-effects is an effect system for Haskell that values expressivity, efficiency, and rigor. It provides an encoding of algebraic, higher-order effects, includes a library of the most common effects, and generates efficient code by fusing effect handlers through computations. It is suitable for use in hobbyist, research, and industrial contexts.

Readers already familiar with effect systems may wish to start with the usage instead. For those interested, this talk at Strange Loop outlines the history of and motivation behind effect systems and fused-effects itself.

Algebraic effects

In fused-effects and other systems with algebraic (or, sometimes, extensible) effects, effectful programs are split into two parts: the specification (or syntax) of the actions to be performed, and the interpretation (or semantics) given to them.

In fused-effects, effect types provide syntax and carrier types provide semantics. Effect types are datatypes with one constructor for each action, invoked using the send builtin. Carriers are monads, with an Algebra instance specifying how an effect’s constructors should be interpreted. Carriers can handle more than one effect, and multiple carriers can be defined for the same effect, corresponding to different interpreters for the effect’s syntax.

Higher-order effects

Unlike some other effect systems, fused-effects offers higher-order (or scoped) effects in addition to first-order algebraic effects. In a strictly first-order algebraic effect system, operations like local or catchError, which specify some action limited to a given scope, must be implemented as interpreters, hard-coding their meaning in precisely the manner algebraic effects were designed to avoid. By specifying effects as higher-order functors, this limitation is removed, meaning that these operations admit a variety of interpretations. This means, for example, that you can introspect and redefine both the local and ask operations provided by the Reader effect, rather than solely ask (as is the case with certain formulations of algebraic effects).

As Nicolas Wu et al. showed in Effect Handlers in Scope, this has implications for the expressiveness of effect systems. It also has the benefit of making effect handling more consistent, since scoped operations are just syntax which can be interpreted like any other, and are thus simpler to reason about.

Fusion

In order to maximize efficiency, fused-effects applies fusion laws, avoiding the construction of intermediate representations of effectful computations between effect handlers. In fact, this is applied as far as the initial construction as well: there is no representation of the computation as a free monad parameterized by some syntax type. As such, fused-effects avoids the overhead associated with constructing and evaluating any underlying free or freer monad.

Instead, computations are performed in a carrier type for the syntax, typically a monad wrapping further monads, via an instance of the Carrier class. This carrier is specific to the effect handler selected, but since it isn’t described until the handler is applied, the separation between specification and interpretation is maintained. Computations are written against an abstract effectful signature, and only specialized to some concrete carrier when their effects are interpreted.

Since the interpretation of effects is written as a typeclass instance which ghc is eager to inline, performance is excellent: approximately on par with mtl.

Finally, since the fusion of carrier algebras occurs as a result of the selection of the carriers, it doesn’t depend on complex RULES pragmas, making it easy to reason about and tune.

Usage

Package organization

The fused-effects package is organized into two module hierarchies:

  • those under Control.Effect, which provide effects and functions that invoke these effects’ capabilities.
  • those under Control.Carrier, which provide carrier types capable of executing the effects described by a given effect type.

An additional module, Control.Algebra, provides the Algebra interface that carrier types implement to provide an interpretation of a given effect. You shouldn’t need to import it unless you’re defining your own effects.

Invoking effects

Each module under the Control.Effect hierarchy provides a set of functions that invoke effects, each mapping to a constructor of the underlying effect type. These functions are similar to, but more powerful than, those provided by mtl. In this example, we invoke the get and put functions provided by Control.Effect.State, first extracting the state and then updating it with a new value:

action1 :: Has (State String) sig m => m ()
action1 = get >>= \ s -> put ("hello, " ++ s)

The Has constraint requires a given effect (here State) to be present in a signature (sig), and relates that signature to be present in a carrier type (m). We generally, but not always, program against an abstract carrier type, usually called m, as carrier types always implement the Monad typeclass.

To add effects to a given computation, add more Has constraints to the signature/carrier pair sig and m. For example, to add a Reader effect managing an Int, we would write:

action2 :: (Has (State String) sig m, Has (Reader Int) sig m) => m ()
action2 = do
  i <- ask
  put (replicate i '!')

Running effects

Effects are run with effect handlers, specified as functions (generally starting with run…) unpacking some specific monad with a Carrier instance. For example, we can run a State computation using runState, imported from the Control.Carrier.State.Strict carrier module:

example1 :: Algebra sig m => [a] -> m (Int, ())
example1 list = runState 0 $ do
  i <- get
  put (i + length list)

runState returns a tuple of both the computed value (the ()) and the final state (the Int), visible in the result of the returned computation. The get function is resolved with a visible type application, due to the fact that effects can contain more than one state type (in contrast with mtl’s MonadState, which limits the user to a single state type).

Since this function returns a value in some carrier m, effect handlers can be chained to run multiple effects. Here, we get the list to compute the length of from a Reader effect:

example2 :: Algebra sig m => m (Int, ())
example2 = runReader "hello" . runState 0 $ do
  list <- ask
  put (length (list :: String))

(Note that the type annotation on list is necessary to disambiguate the requested value, since otherwise all the typechecker knows is that it’s an arbitrary Foldable. For more information, see the comparison to mtl.)

When all effects have been handled, a computation’s final value can be extracted with run:

example3 :: (Int, ())
example3 = run . runReader "hello" . runState 0 $ do
  list <- ask
  put (length (list :: String))

run is itself actually an effect handler for the Lift Identity effect, whose only operation is to lift a result value into a computation.

Alternatively, arbitrary Monads can be embedded into effectful computations using the Lift effect. In this case, the underlying Monadic computation can be extracted using runM. Here, we use the MonadIO instance for the LiftC carrier to lift putStrLn into the middle of our computation:

example4 :: IO (Int, ())
example4 = runM . runReader "hello" . runState 0 $ do
  list <- ask
  liftIO (putStrLn list)
  put (length list)

(Note that we no longer need to give a type annotation for list, since putStrLn constrains the type for us.)

Required compiler extensions

When defining your own effects, you may need -XKindSignatures if GHC cannot correctly infer the type of your constructor; see the documentation on common errors for more information about this case.

When defining carriers, you’ll need -XTypeOperators to declare a Carrier instance over (:+:), -XFlexibleInstances to loosen the conditions on the instance, -XMultiParamTypeClasses since Carrier takes two parameters, and -XUndecidableInstances to satisfy the coverage condition for this instance.

The following invocation, taken from the teletype example, should suffice for most use or construction of effects and carriers:

{-# LANGUAGE FlexibleInstances, GeneralizedNewtypeDeriving, MultiParamTypeClasses, TypeOperators, UndecidableInstances #-}

Defining new effects

The process of defining new effects is outlined in docs/defining_effects.md, using the classic Teletype effect as an example.

Project overview

This project builds a Haskell package named fused-effects. The library’s sources are in src. Unit tests are in test, and library usage examples are in examples. Further documentation can be found in docs.

This project adheres to the Contributor Covenant code of conduct. By participating, you are expected to uphold this code.

Finally, this project is licensed under the BSD 3-clause license.

Development

Development of fused-effects is typically done using cabal v2-build:

cabal v2-build # build the library
cabal v2-test  # build and run the examples and tests

The package is available on hackage, and can be used by adding it to a component’s build-depends field in your .cabal file.

Testing

fused-effects comes with a rigorous test suite. Each law or property stated in the Haddock documentation is checked using generative tests powered by the hedgehog library.

Versioning

fused-effects adheres to the Package Versioning Policy standard.

Benchmarks

To run the provided benchmark suite, use cabal v2-bench. You may wish to provide the -O2 compiler option to view performance under aggressive optimizations. fused-effects has been benchmarked against a number of other effect systems. See also @patrickt’s benchmarks.

Related work

fused-effects is an encoding of higher-order algebraic effects following the recipes in Effect Handlers in Scope (Nicolas Wu, Tom Schrijvers, Ralf Hinze), Monad Transformers and Modular Algebraic Effects: What Binds Them Together (Tom Schrijvers, Maciej Piróg, Nicolas Wu, Mauro Jaskelioff), and Fusion for Free—Efficient Algebraic Effect Handlers (Nicolas Wu, Tom Schrijvers).

Contributed packages

Though we aim to keep the fused-effects core minimal, we encourage the development of external fused-effects-compatible libraries. If you’ve written one that you’d like to be mentioned here, get in touch!

Projects using fused-effects

  • semantic, a program analysis toolkit
  • now-haskell, a client library for AWS Lambda
  • FOSSA, a tool for open-source risk management.

Comparison to other effect libraries

Comparison to mtl

Like mtl, fused-effects provides a library of monadic effects which can be given different interpretations. In mtl this is done by defining new instances of the typeclasses encoding the actions of the effect, e.g. MonadState. In fused-effects, this is done by defining new instances of the Carrier typeclass for the effect.

Also like mtl, fused-effects allows scoped operations like local and catchError to be given different interpretations. As with first-order operations, mtl achieves this with a final tagless encoding via methods, whereas fused-effects achieves this with an initial algebra encoding via Carrier instances.

In addition, mtl and fused-effects are similar in that they provide instances for the monad types defined in the transformers package (Control.Monad.Reader, Control.Monad.Writer, etc). This means that applications using mtl can migrate many existing transformers-based monad stacks to fused-effects with minimal code changes. fused-effects provides its own hierarchy of carrier monads (under the Control.Carrier namespace) that provide a more fluent interface for new code, though it may be useful to use transformers types when working with third-party libraries.

Unlike mtl, effects are automatically available regardless of where they occur in the signature; in mtl this requires instances for all valid orderings of the transformers (O(n²) of them, in general).

Also unlike mtl, there can be more than one State or Reader effect in a signature. This is a tradeoff: mtl is able to provide excellent type inference for effectful operations like get, since the functional dependencies can resolve the state type from the monad type.

Unlike fused-effects, mtl provides a ContT monad transformer; however, it’s worth noting that many behaviours possible with delimited continuations (e.g. resumable exceptions) are directly encodable as effects.

Finally, thanks to the fusion and inlining of carriers, fused-effects is only marginally slower than equivalent mtl code (see benchmarks).

Comparison to freer-simple

Like freer-simple, fused-effects uses an initial encoding of library- and user-defined effects as syntax which can then be given different interpretations. In freer-simple, this is done with a family of interpreter functions (which cover a variety of needs, and which can be extended for more bespoke needs), whereas in fused-effects this is done with Carrier instances for newtypes.

Unlike fused-effects, in freer-simple, scoped operations like catchError and local are implemented as interpreters, and can therefore not be given new interpretations.

Unlike freer-simple, fused-effects has relatively little attention paid to compiler error messaging, which can make common (compile-time) errors somewhat more confusing to diagnose. Similarly, freer-simple’s family of interpreter functions can make the job of defining new effect handlers somewhat easier than in fused-effects. Further, freer-simple provides many of the same effects as fused-effects, plus a coroutine effect, but minus resource management and random generation.

Finally, fused-effects has been benchmarked as faster than freer-simple.

Comparison to polysemy

Like polysemy, fused-effects is a batteries-included effect system capable of scoped, reinterpretable algebraic effects.

As of GHC 8.8, fused-effects outperforms polysemy, though new effects take more code to define in fused-effects than polysemy (though the Control.Carrier.Interpret module provides a low-friction API for rapid prototyping of new effects). Like freer-simple and unlike fused-effects, polysemy provides custom type errors if a given effect invocation is ambigous or invalid in the current context.

Comparison to eff

eff is similar in many ways to fused-effects, but is slightly more performant due to its representation of effects as typeclasses. This approach lets GHC generate better code in exchange for sacrificing the flexibility associated with effects represented as data types. eff also uses the monad-control package to lift effects between contexts rather than implementing an Algebra-style class itself.

Acknowledgements

The authors of fused-effects would like to thank:

  • Tom Schrijvers, Nicholas Wu, and all their collaborators for the research that led to fused-effects;
  • Alexis King for thoughtful discussions about and suggestions regarding our methodology;
  • the authors of other effect libraries, including eff, polysemy, and capabilities, for their exploration of the space.

fused-effects's People

Contributors

alistairb avatar fendor avatar fishtreesugar avatar iand675 avatar jaspa avatar jkachmar avatar johnhampton avatar joshvera avatar kastiglione avatar kivikakk avatar mitchellwrosen avatar ocharles avatar patrickt avatar proofofkeags avatar rl-king avatar robrix avatar shinzui avatar sjoerdvisscher avatar soulomoon avatar symbiont-joseph-kachmar avatar tclem avatar tmcgilchrist avatar turion avatar unam3 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fused-effects's Issues

Eff is slower than the underlying carrier for some operations

In attempting to address #85, I’ve discovered that using Eff with a customized, StateC-isomorphic WriterC is an order of magnitude slower than using WriterC alone for benchmarks of WriterC (Sum Int):

main :: IO ()
main = do
  let i = 100000000
  start <- getCurrentTime
  let !_ = runIt' (replicateM_ i (tell (Sum (1 :: Int))))
  end <- getCurrentTime
  putStrLn $ "run took " ++ show (diffUTCTime end start)
  putStrLn $ show (diffUTCTime end start / fromIntegral i) ++ " per tell"

runIt :: Eff (WriterC (Sum Int) (Eff VoidC)) () -> Sum Int
runIt m = run (execWriter m)

runIt' :: WriterC (Sum Int) Identity () -> Sum Int
runIt' m = runIdentity (execWriterC m)

(Here I’ve defined a Carrier Void Identity instance in addition to the usual Carrier (Writer w :+: sig) (WriterC w m) instance.)

If we benchmark runIt, it takes ~10s, whereas if we benchmark runIt', it takes ~0.1s.

I think this suggests that we should define runWriter & execWriter in terms of WriterC alone, without Eff, and likewise for other carriers which admit Monad instances. On its own, this would have the unfortunate effect of requiring us to define Alternative, MonadRandom, etc. instances for every carrier. However, we could redefine Eff as newtype Eff m a = Eff { unEff :: m a }, and continue defining its Alternative instance in terms of (Carrier sig m, Member NonDet sig) (and similar for the other instances). Finally, we could separately introduce Codensity as a monad-for-free carrier if desired.

Helper macro for writing concrete effect signatures

One of the syntactic compromises of fused-effects compared to @joshvera/effects is that details about the carriers leak into concrete Eff types:

type Something
   = Eff (StateC [Foo]
   ( Eff (ErrorC SomeException
   ( Eff VoidC))))

We could make a Template Haskell splice that could generate the above from some shorthand notation like this:

type Something = [effects | [State [Foo], Error SomeException] |]

VoidC is a terrible name

VoidC has one and only one job: providing run. We should name it PureC or RunC or something instead of VoidC.

MonadIO instance is inconveniently specific

It’s inconvenient that the MonadIO instance for Eff requires Lift IO and not MonadIO m => Lift m. Unfortunately, we can’t generalize it to the latter because the instance head doesn’t mention m and thus it would be ambiguous. (AllowAmbiguousTypes would resolve that, but the ergonomics would be dreadful—any call to liftIO in an Eff context would have to deal with the ambiguity, requiring a lot of extra type annotations.)

We could (potentially) resolve this with some sort of LastMember constraint on the signature, since it could be uniquely determined by the signature and the signature is uniquely determined by the Carrier constraint.

Alternatively, if the continuation of our recent optimization/benchmarking work (cf #90) results in the simplification, deprioritization, or even removal of the Eff type, then we’d likely define MonadIO directly on the carriers, which would have the same result.

cf #82
cf #96 (comment)

Bounded nondeterminism handler

We can generalize runNonDet/runNonDetOnce with runNonDetUpTo :: Maybe Int -> Eff (UpToC f m) a -> m (f a). Backtracking, Interleaving, and Terminating Monad Transformers calls this operator bagofN.

Benchmarks fail to build for 0.2.0.0 on Stackage Nightly

It appears that the benchmarks fail to build for fused-effects-0.2.0.0 with the errors in the snippet below.

Unless there are any objections, I'm going to disable the benchmarks on Stackage's nightly build for the time being; I'll link this issue to that commit so it's easy to keep track of.

Error Snippet

[1 of 1] Compiling Main             ( benchmark/Bench.hs, dist/build/benchmark/benchmark-tmp/Main.o )

benchmark/Bench.hs:15:41: error:
    • Couldn't match type ‘Sum Int’ with ‘Eff VoidC’
      Expected type: Int -> Eff VoidC b0
        Actual type: Int -> Sum Int b0
    • In the second argument of ‘(.)’, namely
        ‘execWriter @_ @_ @(Sum Int) . tellLoop’
      In the first argument of ‘whnf’, namely
        ‘(run . execWriter @_ @_ @(Sum Int) . tellLoop)’
      In the second argument of ‘($)’, namely
        ‘whnf (run . execWriter @_ @_ @(Sum Int) . tellLoop) 100’
   |
15 |       [ bench "100"       $ whnf (run . execWriter @_ @_ @(Sum Int) . tellLoop) 100
   |                                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

benchmark/Bench.hs:15:60: error:
        Actual type: Int -> Sum Int b1
    • In the second argument of ‘(.)’, namely
        ‘execWriter @_ @_ @(Sum Int) . tellLoop’
      In the first argument of ‘whnf’, namely
        ‘(run . execWriter @_ @_ @(Sum Int) . tellLoop)’
      In the second argument of ‘($)’, namely
        ‘whnf (run . execWriter @_ @_ @(Sum Int) . tellLoop) 100000’
   |
16 |       , bench "100000"    $ whnf (run . execWriter @_ @_ @(Sum Int) . tellLoop) 100000
   |                                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

benchmark/Bench.hs:16:60: error:
    • Expected kind ‘* -> *’, but ‘Sum Int’ has kind ‘*’
    • In the type ‘(Sum Int)’
      In the first argument of ‘(.)’, namely
        ‘execWriter @_ @_ @(Sum Int)’
      In the second argument of ‘(.)’, namely
        ‘execWriter @_ @_ @(Sum Int) . tellLoop’
   |
16 |       , bench "100000"    $ whnf (run . execWriter @_ @_ @(Sum Int) . tellLoop) 100000
   |                                                            ^^^^^^^

benchmark/Bench.hs:17:41: error:
    • Couldn't match type ‘Sum Int’ with ‘Eff VoidC’
      Expected type: Int -> Eff VoidC b2
        Actual type: Int -> Sum Int b2
    • In the second argument of ‘(.)’, namely
        ‘execWriter @_ @_ @(Sum Int) . tellLoop’
      In the first argument of ‘whnf’, namely
        ‘(run . execWriter @_ @_ @(Sum Int) . tellLoop)’
      In the second argument of ‘($)’, namely
        ‘whnf (run . execWriter @_ @_ @(Sum Int) . tellLoop) 100000000’
   |
17 |       , bench "100000000" $ whnf (run . execWriter @_ @_ @(Sum Int) . tellLoop) 100000000
   |                                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

benchmark/Bench.hs:17:60: error:
    • Expected kind ‘* -> *’, but ‘Sum Int’ has kind ‘*’
    • In the type ‘(Sum Int)’
      In the first argument of ‘(.)’, namely
        ‘execWriter @_ @_ @(Sum Int)’
      In the second argument of ‘(.)’, namely
        ‘execWriter @_ @_ @(Sum Int) . tellLoop’
   |
17 |       , bench "100000000" $ whnf (run . execWriter @_ @_ @(Sum Int) . tellLoop) 100000000
   |   

Bottom in readme

The readme suggests this is a good implementation:

instance (Carrier sig m, Member (State s) m) => MTL.MonadState s (Wrapper s m) where
  get = get
  put = put

but there is no way that this isn't a bottom :)

runCut creates exponential right-nested branches

runCut interprets NonDet into an underlying Alternative in a way that ends up creating exponential numbers of right-nested branches. Therefore, the problem doesn’t show up with runNonDetOnce, as it terminates immediately on finding a solution in a left-to-right search, but with runNonDet (which explores the entire search space) it’s quite apparent.

Document wrapping Eff for better type inference

It’s often convenient to wrap Eff in a newtype with phantom type parameters for better type inference, but it’s a little non-obvious how to write the handlers & accessors &c. Document this somewhere prominent.

Redundant MonadIO/Member (Lift IO) constraints required in some Carrier instances

If you write a Carrier instance which needs to lift IO actions into the action, you can use a MonadIO constraint on m and then use liftIO as normal. However, if you also have to run an effect handler inside the Carrier instance, on an action which requires MonadIO, then you have to add a superfluous Member (Lift IO) sig constraint, because the effect handler is specified in terms of Eff, and Eff’s MonadIO instance is contingent on a Member (Lift IO) sig constraint. So even tho you have a MonadIO constraint for m, it doesn’t automatically mean you have a MonadIO instance for Eff (Blah m).

We could elide the MonadIO constraint if we didn’t have to run liftIO beside the effect handler (i.e. in m); but sometimes you do have to. We could also elide it if we changed the Carrier instance to operate on Whatever (Eff m) instead of Whatever m. But that’s unidiomatic.

So instead maybe we should offer a sendIO function in Control.Effect.Lift which does the appropriate lifting for us:

sendIO :: (Carrier sig m, Member (Lift IO) sig) => IO a -> m a
sendIO = send . Lift . fmap pure

(This is the same definition as liftIO for Eff.)

We could also generalize it to sendM:

sendM :: (Carrier sig m, Member (Lift n) sig, Monad n) => n a -> m a
sendM = send . Lift . fmap pure

(Technically we could loosen the constraint on n to Functor, but you wouldn’t be able to run the carrier without a Monad instance so there’s not much point in loosening there, AFAICS.)

Note however that similar problems (and potentially similar solutions) exist for the other effects whose only smart constructors are defined using instances on Eff, e.g. Fail/MonadFail, NonDet/Alternative, &c.

Writer effects lack useful operations

WriterT & MonadWriter offer a bunch of useful operations besides tell (types adapted to fit fused-effects conventions):

writer :: (Carrier sig m, Member (Writer w) sig) => (a, w) -> m a
listen :: (Carrier sig m, Member (Writer w) sig) => m a -> m (w, a)
listens :: (Carrier sig m, Member (Writer w) sig) => (w -> b) -> m a -> m (b, a)
pass :: (Carrier sig m, Member (Writer w) sig) => m (w -> w, a) -> m a
censor :: (Carrier sig m, Member (Writer w) sig) => (w -> w) -> m a -> m a

mtl requires MonadWriter instances to provide either tell or writer, as well as listen and pass, defining listens in terms of listen and censor in terms of pass. Based on that, I’m reasonably sure that pass would need to be a scoped operation, and probably listen would as well, since it ranges over the output of its argument.

An interesting use of reflection

I was playing around with fused-effects a bit (basically just trying to understand it), and it occurred to me that it might work very nicely with reflection.

For example, I was designing a simple logging effect, but when it came time to define a carrier for it, I felt I had to make some arbitrary choice about what to do with the message to log.

Since each carrier requires is a separate instance of a type class, this is quite a lot of boilerplate just to get different behaviors.

I noticed this pain as well in fused-effects, with three different trace carriers TraceByReturningC, TraceByPrintingC, and TraceByIgnoringC. In particular, TraceByPrintingC and TraceByIgnoringC could be combined into one TraceC like so:

-- Note: phantom 's'
newtype TraceC s m a = TraceC { runTraceC :: m a }

-- Note: Reifies
instance (Monad m, Carrier sig m, Reifies s (String -> m ())) => Carrier (Trace :+: sig) (TraceC s m) where

-- Note: user supplies the trace function, it is not tucked inside an instance
runTrace 
  :: (Monad m, Carrier sig m) 
  => (String -> m ()) 
  -> (forall s. Eff (TraceC s m) a) 
  -> m a
runTrace f m = reify f (\(_ :: Proxy s) -> runTraceC (interpret (m @s))

Anyways, that's as far as I got, and I'm really not sure if this is unlocking any new power, or is just isomorphic to a using newtype like ReaderT (String -> m ()) m a. Cheers :)

Define a zero-boilerplate generic carrier

We can define a zero-boilerplate carrier parameterized by an effect handler (basically a function suitable for defining eff) as a super simple way to define semantics inline before graduating to a “real” carrier if/when you need more efficiency.

I rejected this idea in an earlier iteration due to overestimating the tolerability of the carrier boilerplate for some users. Now, however, @isovector has persuaded me that this is a useful stepping stone.

Here’s my initial stab at it:

runInterpret :: (forall x . eff m (m x) -> m x) -> InterpretC eff m a -> m a
runInterpret handler = runReader (Handler  handler) . runInterpretC

newtype InterpretC eff m a = InterpretC { runInterpretC :: ReaderC (Handler eff m) m a }
  deriving (Applicative, Functor, Monad)

instance MonadTrans (InterpretC eff) where
  lift = InterpretC . lift

newtype Handler eff m = Handler (forall x . eff m (m x) -> m x)

runHandler :: HFunctor eff => Handler eff m -> eff (InterpretC eff m) (InterpretC eff m a) -> m a
runHandler h@(Handler handler) = handler . handlePure (runReader h . runInterpretC)

instance (HFunctor eff, Carrier sig m) => Carrier (eff :+: sig) (InterpretC eff m) where
  eff (L op) = do
    handler <- InterpretC ask
    lift (runHandler handler op)
  eff (R other) = InterpretC (eff (R (handleCoercible other)))

and here’s it in action:

λ run (runInterpret (\case { Get k -> k 5 ; Put _ k -> k }) get) :: Integer
5

🎩 suggested by @isovector.

asks has an unnecessary Functor constraint

asks is defined in terms of ask and Functor, but we could actually define it solely in terms of the Ask constructor instead and not force Functor constraints on users:

asks :: (Carrier sig m, Member (Reader r) sig) => (r -> a) -> m a
asks f = send (Ask (ret . f))

Document defining effects

Document defining effects, including the effect datatype itself, continuations, HFunctor, Effect, carriers, and handlers. Also higher-order (scoped) effects.

Lazy State carrier

We should offer a lazy State carrier to complement StateC’s strict semantics. Basically this means irrefutable pattern matches for the carrier operations.

AltC Maybe doesn’t terminate for infinitely productive search spaces

run (runNonDet (asum (map pure [1..]))) :: Maybe Int

won’t terminate despite the fact that it should be able to complete immediately on the first result.

This is probably actually “works as expected,” because of the dual semantics of Alternative—there’s no way for AltC to know that <|> for Maybe means “leftmost wins” whereas for [] it means “all win.”

Therefore, this probably warrants the inclusion of a OnceC which returns in Maybe and knows about the <|>-as-choice-not-union semantics.

Random requires n² instances

#111 🔥s Eff, meaning that we will no longer always have a convenient newtype at the top level onto which we can attach all our instances of typeclasses like MonadRandom. Instead, we’ll need to define them for every single carrier type.

They can be derived via GeneralizedNewtypeDeriving for most carriers (composite ones for example), but it’s going to be noisy & annoying to import MonadRandom into every file.

Therefore, I think the Random effect & the MonadRandom instance belong in a new package, with a simple identity monad transformer as a top-level newtype which defines its MonadRandom instance in terms of the Random effect. It’ll be slightly less convenient to use due to the need to wrap things in said newtype, but it at least avoids the _n_² instances problem.

Alternatively, we could define that same newtype in this package, but that wouldn’t allow us to trim our dependencies.

IMO, either way we should add constructors for the effect which don’t rely on MonadRandom instances to Control.Effect.Random.

Note that the same problems technically arise for MonadFail, MonadIO, and Alternative, but since all three of them occur in base it’s harder to make the case for extracting them.

`runError` should use a forall to put its exception type first.

Right now, the runError function puts sig and m before exc in its list of type parameters. This can lead to unpleasantly periphrastic invocations of runError:

runError @_ @_ @FatalException

I propose we add an explicit forall that places the exc at the head of the list, rendering the above

runError @FatalException

I think this keeps with our ethos of “most important type variables first”, but it would be a backwards-incompatible change. Thoughts?

Benchmarks

Benchmark performance of fused handlers for multiple effects, left- and right-associated binds, heavy uses of State, effects above and below NonDet, and so on.

Document kind errors in first-order effects

First-order effects don’t mention m in their constructors, and without PolyKinds or KindSignatures m is inferred to be of kind *, and things break.

Document this error messaging and the solution(s).

Define a zero-boilerplate generic interposition/eavesdropping carrier

Similar to #132’s proposed zero-boilerplate carrier for effects, we can define a zero-boilerplate interposition carrier for effects, allowing you to intercept and possibly respond to requests for other effects.

I’ve been adamantly opposed to adding other definitions of interposition which we’ve made, due to some of the egregious hacks they required, but this stab at it seems to work nicely while avoiding any such hackery:

runInterpose :: (forall x . eff m (m x) -> m x) -> InterposeC eff m a -> m a
runInterpose handler = runReader (Handler handler) . runInterposeC

newtype InterposeC eff m a = InterposeC { runInterposeC :: ReaderC (Handler eff m) m a }
  deriving (Applicative, Functor, Monad)

instance MonadTrans (InterposeC eff) where
  lift = InterposeC . lift

newtype Handler eff m = Handler (forall x . eff m (m x) -> m x)

runHandler :: HFunctor eff => Handler eff m -> eff (ReaderC (Handler eff m) m) (ReaderC (Handler eff m) m a) -> m a
runHandler h@(Handler handler) = handler . handlePure (runReader h)

instance (HFunctor eff, Carrier sig m, Member eff sig) => Carrier sig (InterposeC eff m) where
  eff (op :: sig (InterposeC eff m) (InterposeC eff m a))
    | Just (op' :: eff (InterposeC eff m) (InterposeC eff m a)) <- prj op = do
      handler <- InterposeC ask
      lift (runHandler handler (handleCoercible op'))
    | otherwise = InterposeC (ReaderC (\ handler -> eff (handlePure (runReader handler . runInterposeC) op)))

Here’s a use of it to eavesdrop on requests for a State effect while still using the underlying StateC to respond:

λ runM (runState 5 (runInterpose @(State Integer) (\ op -> liftIO (putStrLn "state op") *> send op) get)) :: IO (Integer, Integer)
state op
( 5 , 5 )

And here’s a use of it to intercept and respond to requests for a State effect without involving the underlying StateC at all:

λ runM (runState 5 (runInterpose @(State Integer) (\case { Get k -> k 42 ; Put _ k -> k }) get)) :: IO (Integer, Integer)
( 5 , 42 )

This would supersede #6 (because we’d document this carrier itself).

Logical cull operator

Picking up from @zenzike’s suggestion
(#65 (comment)), Cut offers call and cut, but we can also express cull as a scoped effect to select only the first child in p.

I think this is the same as the once operator described in Backtracking, Interleaving, and Terminating Monad Transformers, for pruning or “don’t-care nondeterminism.”

Document a list of useful default extensions

Splitting this out as a todo ticket from discussion in #55:

Perhaps it would be good to have a list in the docs of language extensions you'll probably need to enable to use this library. In general, I think this is a useful kind of documentation, which I haven't seen in many libraries. While Hackage lists the language extensions necessary to implement a library, it doesn't have a mechanism to directly document which ones you need as an end-user of the library (and for what purposes).

Document interposition using prj &c.

It’s sometimes convenient to interpose a handler for an effect without necessarily removing the capacity for said effect. This can be accomplished with one-off interposition carriers (unfortunately there doesn’t seem to be a convenient way to make a parameterized interpose operator, due to how fusion works). Document this.

Embedding monad transformers in carrier stacks is fiddly

It’s tricky to define Carriers which embed non-Carrier monad transformers, and even more so if these exist within other embedded carriers. E.g.

newtype REPLC cmd m a = REPLC { runREPLC :: ReaderC (Parser (Maybe cmd)) (ReaderC Line (InputT m)) a }
  deriving (Applicative, Functor, Monad, MonadIO)

The Carrier instance has to lift InputT expressions through two ReaderCs, which don’t have MonadTrans instances:

str <- ReaderC (const (ReaderC (const (getInputLine (cyan <> prompt <> plain)))))

and lifting computations inside the InputT requires even deeper nesting:

res <- ReaderC (const (ReaderC (const (lift (runError (traverse (parseString (whole c) (lineDelta l)) str))))))

Running higher-order effects is even worse, requiring joins and lifts and more manual construction of ReaderCs.

mtl compatibility

In practical scenarios it's often required to combine fused-effects with mtl-based stacks.

As an example, let's imagine having an involved transformer stack that we'd like to run our fused-effects stack on top of -- while also transparently using the operations from the underlying transformer in the fused-effects stack.

Is this currently possible -- at least for pure fused-effects, like Reader?

Case in point:

doit ∷ (MonadIO m) ⇒ m (String, String)
doit =
  MTL.runReaderT (do
    liftIO $ runM $ runReader "quux" $ do
      liftIO $ putStrLn "foo"
      foo ← MTL.ask
      bar ← ask
      pure (foo, bar))
    ["bar"]

->

error:
    • Couldn't match type ‘MTL.ReaderT String m0’
                     with ‘Eff (ReaderC r0 (Eff (LiftC IO)))’
      Expected type: Eff (ReaderC r0 (Eff (LiftC IO))) String
        Actual type: MTL.ReaderT String m0 String
    • In a stmt of a 'do' block: foo <- MTL.ask
      In the second argument of ‘($)’, namely
        ‘do liftIO $ putStrLn "foo"
            foo <- MTL.ask
            bar <- ask
            return (foo, bar)’
      In the second argument of ‘($)’, namely
        ‘runReader "quux"
           $ do liftIO $ putStrLn "foo"
                foo <- MTL.ask
                bar <- ask
                return (foo, bar)’

Listing carrier instances first in function contexts leads to poor -XTypeApplications interactions

Ideally we’d be able to do something like runWriter @(Sum Int) with -XTypeApplications in order to specify w without having to specify any of the other stuff. However, runWriter’s signature has its context ordered alphabetically:

runWriter :: (Carrier sig m, Effect sig, Functor m, Monoid w) => Eff (WriterC w m) a -> m (w, a)

requiring us to skip two other type arguments first:

runWriter @_ @_ @(Sum Int)

or alternately match against the sig parameter:

runWriter @(Writer (Sum Int) :+: _)

By contrast, tell orders its context to place the Writer parameter first:

tell :: (Member (Writer w) sig, Carrier sig m) => w -> m ()

This is less of a problem for runReader and runState since both of those take a value of the type parameter as their first argument.

We should go through all the functions for parameterized effects (at a minimum, Writer, Reader, and State) and make sure that the type parameters occur in a sensible order. We might need to use explicit foralls in some places to guarantee that this should be so.

Be pickier about re-exports from Control.Effect

I think we should be pickier about re-exporting modules from Control.Effect. It’s one thing to export the effect types (which are carried by type signatures and thus propagate far and wide), and another thing to export all the constructors, smart constructors, and handlers.

We could maybe offer a Control.Effect.Everything module if the latter is considered all that useful, but for the moment I think a middleground would be a better fit.

Propagating MonadIO constraint downwards

How does one use the underlying monad that is MonadIO-constrained using runM?

On the first approach it doesn't work (full exhibit at [1]):

doliftio :: MonadIO m => m ()
doliftio = runM (liftIO $ putStr "")

..yields..

test/Control/Effect/LiftIO.hs:26:18: error:
    ⢠Could not deduce (Member (Lift IO) (Lift m))
        arising from a use of âliftIOâ
      from the context: MonadIO m
        bound by the type signature for:
                   doliftio :: forall (m :: * -> *). MonadIO m => m ()
        at test/Control/Effect/LiftIO.hs:25:1-29
    ⢠In the first argument of ârunMâ, namely â(liftIO $ putStr "")â
      In the expression: runM (liftIO $ putStr "")
      In an equation for âdoliftioâ: doliftio = runM (liftIO $ putStr "")
   |
28 | doliftio = runM (liftIO $ putStr "")
   |                  ^^^^^^^^^^^^^^^^^^

--

  1. https://github.com/deepfire/fused-effects/blob/2a10f9351d8602b6c32b7af8830d58c76dad0655/test/Control/Effect/LiftIO.hs

AltC is unlawful for non-communative monads

#111 makes AltC a monad, but since it’s essentially the same as ListT, it’s only lawful when applied to commutative monads. We should use a lawful backtracking monad implementation instead.

Batteries-included lifting of handlers into effects

FreshC is StateC-like in how it gets lifted into other effects. ResumableWithC is ReaderC-like in the same regard. These are common enough patterns that they might warrant definitions like:

handleStatelike s run = eff . handle (s, ()) (uncurry (flip run))
handleReaderlike r run = eff . handlePure (flip run r)

We could further flesh this out with handlers for reinterpret, reinterpret2, etc., if we wanted.

cf #23 (comment)

Feature request: allow catching of errors thrown from IO

Right now, it's not necessarily clear that Control.Effect.Error can't capture errors that happen in IO. The following code looks like it should return a Left SomeException value, since it runs with Error SomeException as a constraint, but if we throw an error inside a Lift IO effect the exception is not caught:

runM . runError @_ @_ @Exc.SomeException $ (() <$ liftIO (ioError (userError "boom")))
-- *** Exception: user error (boom)

I have a branch where I add a catchIO function that handles this properly, using the Resource effect to unlift Control.Exception.catch to an effectful context:

catchIO :: (Member Resource sig, Carrier sig m)
        => m a
        -> (forall e . Exc.Exception e => e -> m a)
        -> m a

runM . runResource runM $ catchIO (True <$ liftIO (ioError (userError "boom"))) (const (pure False))
-- False

I am currently working inside an effect stack that delegates often to IO actions, and every time I call liftIO I open myself to the possibility of unhandled exceptions and early termination of my program, so this functionality is a big win to me. I can't see much downside to adding this, though it's arguable that it deserves its own effect rather than being foisted into Resource, and that catchIO is not the best name. I don't think catch is appropriate, given that we already provide catchError.

Define a teletype effect in the README/examples component

Here’s the definition @rewinfrey & I came up with:

teletype :: Spec
teletype = describe "teletype" $ do
  prop "reads" $
    \ line -> run (runTeletypeRet [line] read) `shouldBe` (([], []), line)

  prop "writes" $
    \ input output -> run (runTeletypeRet input (write output)) `shouldBe` ((input, [output]), ())

  prop "writes multiple things" $
    \ input output1 output2 -> run (runTeletypeRet input (write output1 >> write output2)) `shouldBe` ((input, [output1, output2]), ())

data Teletype m k
  = Read (String -> k)
  | Write String k
  deriving (Functor)

instance HFunctor Teletype where
  hfmap _ (Read    k) = Read    k
  hfmap _ (Write s k) = Write s k

read :: (Member Teletype sig, Carrier sig m) => m String
read = send (Read gen)

write :: (Member Teletype sig, Carrier sig m) => String -> m ()
write s = send (Write s (gen ()))


runTeletypeIO :: (MonadIO m, Carrier sig m) => Eff (TeletypeIOH m) a -> m a
runTeletypeIO = runTeletypeIOH . interpret

newtype TeletypeIOH m a = TeletypeIOH { runTeletypeIOH :: m a }

instance (MonadIO m, Carrier sig m) => Carrier (Teletype :+: sig) (TeletypeIOH m) where
  gen = TeletypeIOH . gen
  alg = algT \/ algOther
    where algT (Read    k) = TeletypeIOH (liftIO getLine >>= runTeletypeIOH . k)
          algT (Write s k) = TeletypeIOH (liftIO (putStrLn s) >> runTeletypeIOH k)
          algOther op = TeletypeIOH (alg (handlePure runTeletypeIOH op))


runTeletypeRet :: Effectful sig m => [String] -> Eff (TeletypeRetH m) a -> m (([String], [String]), a)
runTeletypeRet input = flip runTeletypeRetH input . interpret

newtype TeletypeRetH m a = TeletypeRetH { runTeletypeRetH :: [String] -> m (([String], [String]), a) }

instance Effectful sig m => Carrier (Teletype :+: sig) (TeletypeRetH m) where
  gen a = TeletypeRetH (\ i -> gen ((i, []), a))
  alg = algT \/ algOther
    where algT (Read    k) = TeletypeRetH (\ i -> case i of
            []  -> runTeletypeRetH (k "") []
            h:t -> runTeletypeRetH (k h)  t)
          algT (Write s k) = TeletypeRetH (\ i -> do
            ((i, out), a) <- runTeletypeRetH k i
            pure ((i, s:out), a))
          algOther op = TeletypeRetH (\ i -> alg (handle ((i, []), ()) mergeResults op))
          mergeResults ((i, o), m) = do
            ((i', o'), a) <- runTeletypeRetH m i
            pure ((i', o ++ o'), a)

TemplateHaskell-generated smart constructors

We could relieve some of the tedium of defining new effects by using TemplateHaskell to generate smart constructors.

I think this might belong in another package in the org; I kinda like keeping fused-effects’ dependencies minimal. Thoughts?

🎩 @isovector for the suggestion.

Add examples of carrier composition

In 0.3, carrier composition is simpler, faster, and more convenient than in earlier versions. We should add at least one example of defining a carrier by composing other carriers together.

This is closely related to but (IMO) distinct from #9, which is about the simpler case of defining a carrier using a single underlying carrier; e.g. FreshC being defined using StateC.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.