Git Product home page Git Product logo

capability's Introduction

capability: effects, extensionally

Build status

A capability is a type class that says explicitly which effects a function is allowed to use. The mtl works like this too. But unlike the mtl, this library decouples effects from their implementation. What this means in practice:

  • You can implement large sets of capabilities using the efficient ReaderT pattern, rather than a slow monad transformer stack.
  • Capabilities compose well: e.g. it's easy to have multiple reader effects.
  • You can use a writer effect without implementing it as a writer monad (which is known to leak space).
  • You can reason about effects. For instance, if a monad provides a reader effect at type IORef A, it also provides a state effect at type A

For more on these, you may want to read the announcement blog post.

This library is an alternative to the mtl. It defines a set of standard, reusable capability type classes, such as the HasReader and HasState type classes, which provide the standard reader and state effects, respectively.

Where mtl instances only need to be defined once and for all, capability-style programming has traditionally suffered from verbose boilerplate: rote instance definitions for every new implementation of the capability. Fortunately GHC 8.6 introduced the DerivingVia language extension. We use it to remove the boilerplate, turning capability-style programming into an appealing alternative to mtl-style programming. The generic-lens library is used to access fields of structure in the style of the ReaderT pattern.

An additional benefit of separating capabilities from their implementation is that they avoid a pitfall of the mtl. In the mtl, two different MonadState are disambiguated by their types, which means that it is difficult to have two MonadState Int in the same monad stack. Capability type classes are parameterized by a name (also known as a tag). This makes it possible to combine multiple versions of the same capability. For example,

twoStates :: (HasState "a" Int m, HasState "b" Int m) => m ()

Here, the tags "a" and "b" refer to different state spaces.

In summary, compared to the mtl:

  • capabilities represent what effects a function can use, rather than how the monad is constructed;
  • capabilities are named, rather than disambiguated by type;
  • capabilites are discharged with deriving-via combinators and generic-lens, rather than with instance resolution.

An example usage looks like this:

testParity :: (HasReader "foo" Int m, HasState "bar" Bool m) => m ()
testParity = do
  num <- ask @"foo"
  put @"bar" (even num)

data Ctx = Ctx { foo :: Int, bar :: IORef Bool }
  deriving Generic

newtype M a = M { runM :: Ctx -> IO a }
  deriving (Functor, Applicative, Monad) via ReaderT Ctx IO
  -- Use DerivingVia to derive a HasReader instance.
  deriving (HasReader "foo" Int, HasSource "foo" Int) via
    -- Pick the field foo from the Ctx record in the ReaderT environment.
    Field "foo" "ctx" (MonadReader (ReaderT Ctx IO))
  -- Use DerivingVia to derive a HasState instance.
  deriving (HasState "bar" Bool, HasSource "bar" Bool, HasSink "bar" Bool) via
    -- Convert a reader of IORef to a state capability.
    ReaderIORef (Field "bar" "ctx" (MonadReader (ReaderT Ctx IO)))

example :: IO ()
example = do
    rEven <- newIORef False
    runM testParity (Ctx 2 rEven)
    readIORef rEven >>= print
    runM testParity (Ctx 3 rEven)
    readIORef rEven >>= print

For more complex examples, see the Examples section and the examples subtree.

API documentation can be found on Hackage.

Examples

An example is provided in WordCount. Execute the following commands to try it out:

$ nix-shell --pure --run "cabal configure --enable-tests"
$ nix-shell --pure --run "cabal repl examples"

ghci> :set -XOverloadedStrings
ghci> wordAndLetterCount "ab ba"
Letters
'a': 2
'b': 2
Words
"ab": 1
"ba": 1

To execute all examples and see if they produce the expected results run

$ nix-shell --pure --run "cabal test examples --show-details=streaming --test-option=--color"

Build instructions

Nix Shell

A development environment with all dependencies in scope is defined in shell.nix.

Build

The build instructions assume that you have Nix installed. Execute the following command to build the library.

$ nix-shell --pure --run "cabal configure"
$ nix-shell --pure --run "cabal build"

capability's People

Contributors

aherrmann avatar aspiwack avatar byorgey avatar curiousleo avatar ericson2314 avatar guibou avatar mboes avatar mijothy avatar mizunashi-mana avatar mrkkrp 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  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

capability's Issues

Orphan instances might be required on intermediate newtypes

E.g. in the following code

class Monad m => Logger m where
  logStr :: String -> m ()

newtype TheLoggerReader m a = TheLoggerReader (m a)
  deriving (Functor, Applicative, Monad)
instance  (HasReader "logger" (String -> IO ()) m, MonadIO m) => Logger (TheLoggerReader m)

newtype CountLogM m a = CountLogM (ReaderT CountLogCtx m a)
  deriving (Functor, Applicative, Monad)
  deriving Logger via
    (TheLoggerReader (Field "logger" (Field "logCtx" (MonadReader (ReaderT CountLogCtx m)))))

the deriving Logger via clause requires MonadIO instances for Field and MonadReader.
It might happen that a user is forced to define orphan instances for newtypes like MonadReader for type-classes that where not anticipated in this package. This may be unavoidable. However, we should make sure to systematically provide instances for a set of select common type-classes for all newtypes defined in this package.

See #4 (comment)

Add `HasWriter` capabilities

With the using MonadWriter logic.

However, I suggest to not provide implementations in terms of MonadWriter at all. For this is generally a bad implementation. Instead, the basic instance should be from a HasState instance.

HasMask capability

Should we add a HasMask capability to complement HasThrow, and HasCatch, similar to MonadMask?

If so, what would the meaning of the tag be? Should it only select a layer in a transformer stack (if applicable), or should it decide which exceptions to mask? The latter seems to contradict the purpose of bracket.

See #25 (comment).

cc @aspiwack

Generalise IO-based instances to also work with ST

This is really not urgent. But let's not forget about it.

We could either make combinators to work with ST. Or maybe abstract this away using something like PrimMonad. I believed there was some kind of abstract Ref (or something) type family, which evaluated to IORef for IO and STRef for ST. But I can't find it anymore. Have I dreamed it?

Also, should we consider STM and TVars. How about MVars for IO?

Document Nix cache in README

It currently takes a long time to get started with this package. Running the nix-shell for the first time requires compiling a long list of patched up dependencies. I didn't time it but estimate it took at least 45 minutes on my machine. That's not a very friendly experience, so we should make it easy for the user to get started quickly and explain how.

`nix-shell` fails

Nice library. I'm enjoying using it, but I'd like to play around with it a bit. Unfortunately, running nix-shell --verbose results in the following error:

evaluating file '/nix/store/k21q9k2bmb7qj2kq6md0xgs8d274nvws-nix-2.2/share/nix/corepkgs/derivation.nix'
evaluating file '/home/james/coding/projects/capability/nix/default.nix'
evaluating file '/home/james/coding/projects/capability/nix/nixpkgs/default.nix'
evaluating file '/nix/store/k21q9k2bmb7qj2kq6md0xgs8d274nvws-nix-2.2/share/nix/corepkgs/fetchurl.nix'
evaluating file '/nix/store/k21q9k2bmb7qj2kq6md0xgs8d274nvws-nix-2.2/share/nix/corepkgs/config.nix'
building '/nix/store/afh5rs4gk1jrsm6li8jgik0790bkg98f-89b618771ad4b0cfdb874dee3d51eb267c4257dd.tar.gz-unpacked.drv'...
while setting up the build environment: executing '/nix/store/cinw572b38aln37glr0zb8lxwrgaffl4-bash-4.4-p23/bin/bash': No such file or directory
builder for '/nix/store/afh5rs4gk1jrsm6li8jgik0790bkg98f-89b618771ad4b0cfdb874dee3d51eb267c4257dd.tar.gz-unpacked.drv' failed with exit code 1
error: build of '/nix/store/afh5rs4gk1jrsm6li8jgik0790bkg98f-89b618771ad4b0cfdb874dee3d51eb267c4257dd.tar.gz-unpacked.drv' failed

According to a post in the link in nix/nixpkgs/default.nix (NixOS/nixpkgs#30399 (comment)), using builder = nixcfg.shell; "works ... because of the /bin/sh Nix impurity", and seems to be the source of my issue.

I see two ways of fixing this:

  1. Change the line to builder = builtins.storePath nixcfg.shell;.
  2. Use the nix2 idiomatic way of pinning nixpkgs.

I'm happy to do either of these and PR.

Cheers

Cannot Lift through Stream

Attempting to apply the Lift strategy on Stream (Of a) to access an underlying MonadState produces a type error in a coercion. In the example to derive HasSource, the same error occurs for HasSink and HasState.

newtype MyStates a = MyStates (Stream (Of String) (State Bool) a)
  deriving (Functor, Applicative, Monad)
  deriving (HasSource () Bool) via
    Lift (Stream (Of String) (MonadState (State Bool)))
    • Couldn't match type ‘StateT Bool Data.Functor.Identity.Identity’
                     with ‘MonadState (State Bool)’
        arising from the coercion of the method ‘await_’
          from type ‘ghc-prim-0.5.3:GHC.Prim.Proxy# ()
                     -> Lift (Stream (Of String) (MonadState (State Bool))) Bool’
            to type ‘ghc-prim-0.5.3:GHC.Prim.Proxy# () -> MyStates Bool’
    • When deriving the instance for (HasSource () Bool MyStates)
   |
70 |   deriving (HasSource () Bool) via
   |             ^^^^^^^^^^^^^^^^^

This has been observed on master with GHC 8.6.5 and GHC 8.8.1.

The same issue is also present in version 0.2.0.0 of capability with an adjusted reproduction

newtype MyStates a = MyStates (Stream (Of String) (State Bool) a)
  deriving (Functor, Applicative, Monad)
    deriving (HasState () Bool) via
        Lift (Stream (Of String) (MonadState (State Bool)))

INLINE Pragmas

I think all the methods should have INLINE pragmas: many of them are just coercions, which inline well, and more importantly combine well, but even the other are quite short and should compose well. And since we are typically combining 3 or 4 layers of these, we would pay an unnecessary cost with respect to hand written instances.

Add Error capability

As requested in #17 (comment) .

Some points for discussion:

  1. Is a tag meaningful and useful?
    It could be used to select a layer in a transformer stack. E.g.
    example :: ExampleM ()
    example = do
      let catchIO = catch @"io"
      throw @"either" MyException
        `catchIO` \MyExcetion -> pure ()
    
    newtype ExampleM a = ExampleM (ExceptT MyException IO)
  2. Should we follow Control.Monad.Catch and have separate capabilities for throw/catch/mask, or rather one class for all?
    Separate classes seem more flexible. E.g. throw works in cases where mask doesn't.
  3. Should the error type be an index to HasError?
    If HasError should cover e.g. ExceptT then, it seems, the answer should be yes.
    The general IO case could be covered with a synonym HasError_ where e ~ SomeException.

cc @aspiwack

Relationship between tags and record field names

In the current implementation the instance tag, i.e. tag in HasState tag s m, is unrelated to any potential record field labels in the state type. E.g. the following instances (MonadState, and Field)

instance State.MonadState s m => HasState tag s (MonadState m)
instance ( Generic s', Generic.HasField' field s' s, HasState tag s' m )
  => HasState tag s (Field field m)

exist for any tag.

Consider the following example.

data MyState = MyState { msFoo :: Int }
  deriving Generic

newtype MyStateM a = MyStateM (State MyState a)
  deriving (Functor, Applicative, Monad)
  deriving (HasState "foo" Int) via
    Field "msFoo" (MonadState (State MyState))

The field label msFoo and the tag foo are unrelated.

Alternatively, we could enforce that the tag and record field match. For nested records, or other situations where this may not be the case, we could introduce a Rename combinator. The above example could then look like this.

newtype MyStateM' a = MyStateM' (State MyState a)
  deriving (Functor, Applicative, Monad)
  deriving (HasState "foo" Int) via
    Rename "foo" (Field "msFoo" (MonadState (State MyState)))

where MonadState would still exist for any tag, but Field requires that tag and field match.

As a side note, for non-records a Position combinator could be introduced that uses HasPosition' from generic-lens.

See #7 (comment)

cc @aspiwack

Consider renaming library

I assume capabilities-via was a temporary name. It's leaking an implementation detail that, many years in the future or sooner than that even, will be largely immaterial.

There is already an (unmaintained) package called Capabilities. We could claim capability, with the view towards putting all modules currently at top-level under a Capability.* namespace.

GHC 8.8 compatible version

master passes the tests on stack nightly, but the hackage release does not.

These bumped bounds are also needed

    , generic-lens >= 1.0 && < 1.3
    , primitive >= 0.6 && < 0.8

Move all modules under a single namespace

There are currently many top-level modules. For an application, this is not a problem. But for a library, polluting the top-level namespace can be a problem. For starters, we can move each capability module. Example: Capability.HasReader.

`zoom`, `magnify`, and `wrapError` forget other capabilities

The functions zoom, magnify, and wrapError as introduced by #31 allow to modify a HasState, HasReader, or HasCatch/Throw capability according to a deriving strategy. However, they forget any other capabilities that were in scope before. E.g. in the following code the writer capability is forgotten:

verboseStates
  :: (HasState "both" (Int, Int) m, HasWriter "log" (Sum Int) m)
  => m ()
verboseStates = do
  let incAndTell
        :: (HasState "count" Int m, HasWriter "log" (Sum Int) m)
        => m ()
      incAndTell = do
        msg <- state @"count" (\n -> (n, succ n))
        tell @"log" (Sum msg)
  zoom @"both" @"count" @(Rename 1 :.: Pos 1 "both") $
    -- XXX: Cannot use incAndTell here, because the HasWriter has been forgotten.
    -- incAndTell
    pure ()

A more general combinator, call it using, would allow the above. E.g.

using
  @'[ HasState "count" Int `Via` Rename 1 :.: Pos 1 "both"
    , HasWriter "log" (Sum Int) `Via` Self ] $
  -- Can use incAndTell here
  incAndTell

It could be implemented as follows:

using :: forall vias m a. AllCapabilities vias (Combine vias m)
  => (forall m'. AllCapabilities vias m' => m' a)
  -> m a
using m = coerce @(Combine vias m a) m

newtype Combine (vias :: [*]) m (a :: *) = Combine (m a)
  deriving (Functor, Applicative, Monad, MonadIO, PrimMonad)

-- Add appropriate capability instances for `Combine`. E.g.
deriving via ((t :: (* -> *) -> * -> *) m)
  instance
    ( NoDuplicateStrategy (HasState tag s) vias
    , forall x. Coercible (m x) (t m x)
    , HasState tag s (t m)
    , Monad m )
    => HasState tag s (Combine (HasState tag s `Via` t ': vias) m)
deriving via (Combine vias m)
  instance {-# OVERLAPPABLE #-}
    ( HasState tag s (Combine vias m), Monad m )
    => HasState tag s (Combine (via ': vias) m)


data Via
  (capability :: (* -> *) -> Constraint)
  (strategy :: (* -> *) -> * -> *)
infix 8 `Via`

newtype Self m (a :: *) = Self (m a)
  deriving (Functor, Applicative, Monad, MonadIO, PrimMonad)

An alternative way of managing effects in `capability`

(I'm not sure if this is the right place for suggesting this; if it isn't appropriate feel free to close this)

In capability (and mtl), the pattern of restricting what a computation can do is by imposing constraints on a polymorphic monad type:

app :: HasState MyState m => m ()

As Alexis King pointed out, this approach has inevitable performance cost. On the other hand, using a concrete monad admits more optimizations, but it automatically allows you to do all operations implemented for that monad, so restriction is not possible:

newtype App a = App { runApp :: Reader Env IO a }
  deriving (Functor, Applicative, Monad, MonadIO)
  deriving (HasSource "r" SomeType) via ...
  deriving (HasSink "s" SomeOtherType) via ...
app :: HasSink "s" SomeOtherType App => App ()
-- The constraint is redundant; it does not prevent the code
-- from using Source functionalities either

We can address the problem using the phantom constraint pattern. Specifically, we can define a "barrier monad transformer" M and a phantom typeclass Eff:

newtype M m a = UnsafeM (m a)
  deriving (Functor, Applicative, Monad)
class Eff (e :: Capability)

deriving instance (MonadIO m, Eff MonadIO) => MonadIO (M m)
deriving instance (HasSink tag a m, Eff (HasSink tag a)) => HasSink tag a (M m)
deriving instance (HasSource tag a m, Eff (HasSource tag a)) => HasSource tag a (M m)
...

such that this won't typecheck:

app :: M App SomeType
app = await @"r"

instead, one need to introduce an Eff constraint:

app' :: Eff (HasSource "r" SomeType) => M App SomeType
app' = await @"r"

This pattern allows effect management on concrete monads to a great extent and works particularly well with capability (it also works mtl, unliftio and exceptions). In microbenchmarks, this kind of usage often performs better than polymorphic code (and never performs worse); I believe that'll also be true in real use. The obvious downside though, is that by using a concrete monad, it invalidates the usage of Capability.Reflection and Capability.Derive.

I have already written a small demo library that simplifies the boilerplates away: [hackage, repo]. I'd like to know about your thoughts on this pattern (and in particular, the possibility of integrating this into capability).

Running effects locally

While other effect libraries, no matter which approach, uses an effect stack that may change in different parts of programs, capability encourages the use of one single concrete monad to handle all effects. This is good for performance, however also means that there is no effective way to "run" an effect anyhow, except unwrapping the whole monad.

Oftentimes, a user (i.e. me) has a function simple that may raise an exception, but it is catched and handled in another function, say complex, so complex by itself won't raise any exception.

In a conventional library like freer-simple, I could write:

simple :: (Member (Error MyErr) m) => Int -> Eff m ()
complex :: Int -> Eff m ()
complex n = do
  ...
  result <- runError $ simple n
  case result of
    Left e -> ...
    Right x -> ...

But there is no similar functionality to runError in capability because it has a fixed monad. If simple has HasThrow ... then complex must have HasThrow too, which does not model the behavior correctly.

Is there any existing idiom tackling this problem? If not, could we have a typeclass that simulates "running effects" on a fixed monad locally?

Capability focusing using lens?

Hi, apologies, this is neither a feature request nor a bug report. But a question that I did not know where else to ask.

Is there a way to derive a capability using a lens into a state? I understand that the Field/HasField and Pos/HasPos capabilities relate to their lens counterpats. But in my case I have a lens

view :: a -> b 
view = ...
update :: a -> b -> a
update = ...

and a capability HasState "stateName" b and I would like to turn it into a HasState "stateName" a using the lens described above. The ideal type signature would be something like
focus :: HasState t b m => Lens a b -> (forall m' . HasState t a m' => m' r) -> m r

Is there a way to achieve this?

Intuitively, I would assume zoom achieves that but I do not see what transformer to use or how to even define one.

`listen` and `pass` breaks on error or with concurrency

The current definitions of listen and pass are:

instance (Monoid w, HasState tag w m)
  => HasWriter tag w (WriterLog m)
  where
    writer_ tag (a, w) = yield_ tag w >> pure a
    {-# INLINE writer_ #-}
    listen_ :: forall a. Proxy# tag -> WriterLog m a -> WriterLog m (a, w)
    listen_ _ m = coerce @(m (a, w)) $ do
      w0 <- get @tag
      put @tag mempty
      a <- coerce m
      w <- get @tag
      put @tag $! w0 <> w
      pure (a, w)
    {-# INLINE listen_ #-}
    pass_ :: forall a. Proxy# tag -> WriterLog m (a, w -> w) -> WriterLog m a
    pass_ _ m = coerce @(m a) $ do
      w0 <- get @tag
      put @tag mempty
      (a, f) <- coerce @_ @(m (a, w -> w)) m
      w <- get @tag
      put @tag $! w0 <> f w
      pure a
    {-# INLINE pass_ #-}

There is no error recovery at all, and by temporarily "repurposing" the mutable state, other threads end up not writing to where they should be writing.

The concurrency problem may not be worth fixing (in some sense), but at least it deserves a warning in the docs; having no error recovery is arguably a larger problem.

Overlapping tags

The question whether tags may overlap on the same object/transformer-layer came up in the context of HasReader, HasState, and HasError.


In the case of HasReader different tags may address the same transformer layer, as long as they address different components in that one layer. E.g.

newtype GoodReader a = GoodReader { runGoodReader :: (Int, Int) -> IO a }
  deriving (Functor, Applicative, Monad, MonadIO) via ReaderT (Int, Int) IO
  deriving (HasReader "foo" Int) via Pos 1 (MonadReader (ReaderT (Int, Int) IO))
  deriving (HasReader "bar" Int) via Pos 2 (MonadReader (ReaderT (Int, Int) IO))

newtype BadReader a = GoodReader { runBadReader :: Int -> IO a }
  deriving (Functor, Applicative, Monad, MonadIO) via ReaderT Int IO
  deriving (HasReader "foo" Int) via MonadReader (ReaderT Int IO)
  deriving (HasReader "bar" Int) via MonadReader (ReaderT Int IO)

example :: (HasReader "foo" Int m, HasReader "bar" Int m, MonadIO m) => m ()
example = do
  liftIO . print =<< ask @"bar"
  local @"foo" (const 2) $
    liftIO . print =<< ask @"bar"

ghci> runGoodReader example (1, 1)
1
1
ghci> runBadReader example 1
1
2

With HasState the issue is very similar. Additional concerns appear in the context of concurrency.

For both HasReader, and HasState it seems reasonable to simply state that deriving instances such as those of BadReader above is illegal.


How should HasError handle similarly overlapping instances?

Consider the following program where foo may throw SomeError under the tag "foo" and bar may throw or catch under the tag "bar".

data SomeError = SomeError
foo :: HasThrow "foo" SomeError m => m ()
bar :: HasCatch "bar" SomeError m => m () -> m ()

combined :: (HasCatch "foo" SomeError m, HasCatch "bar" SomeError m) => m ()
combined = bar foo

It may be very important that bar does not secretly catch and silence an error thrown under the tag "foo" in combined.

The current implementation does not take this into account and bar would indeed catch an exception thrown under foo.

A possible solution might be to internally tag exceptions by wrapping them in Tagged below before throwing. The corresponding catch would then only catch exceptions with a matching tag.

newtype Tagged (tag :: k) (e :: *) = Tagged e

This works when exceptions are based on IO exceptions. Note, however, it doesn't work with MonadError e (Either e) or MonadError IO Maybe. In that case, a valid approach might be to not tag, and forbid colliding instances.

On a side note, Tagged may not integrate well with Rename (see #8). E.g.

newtype BadExcept a = IO a
  deriving HasThrow "foo" SomeError via Rename "foo" "base" (MonadUnliftIO SomeError IO)
  deriving HasThrow "bar" SomeError via Rename "bar" "base" (MonadUnliftIO SomeError IO)
  deriving HasCatch "foo" SomeError via Rename "foo" "base" (MonadUnliftIO SomeError IO)
  deriving HasCatch "bar" SomeError via Rename "bar" "base" (MonadUnliftIO SomeError IO)

would derive colliding instances for HasThrow, and HasCatch.

cc @aspiwack

Is `derive` safe?

This is somehow a philosophical question. I believe that the capabilities instances of a monad restricts what user could possibly do within the monad. However derive breaks this promise: For a monad, we can now create and use a capability on the fly, all of which is not reflected on the return type.

So, is derive “unsafe” in this sense? Should we make it internal and only for use with safer functions like wrapError, or add some form of warning to its documentation?

Add `HasStream` capability

Or HasGenerator if we want to sound pythony. This would have a single method

yield :: HasStream t a m => a -> m ()

It's also a common capability. When in need, it's often implemented as a writer of [a], but that's quadratic. Instead, the basic instances should be in terms of HasState t [a] m (with adding the yielded values to the beginning of the list), and of Stream from the streaming package.

Enclosing call to 'local' resets accumulated Writer value when the values share a record

Describe the bug
Sometimes an enclosing call to 'local' for a particular Reader capability causes the accumulated value for a (conceptually unrelated) Writer capability to be reset. This only seems to happen when the concrete monad implementing the capabilities stores the values in the same record, though it's possible the record thing is a red herring.

To Reproduce

Below is the smallest example I have been able to come up with.

{-# LANGUAGE DataKinds                  #-}
{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE DerivingVia                #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE GeneralisedNewtypeDeriving #-}
{-# LANGUAGE TypeApplications           #-}

import qualified Capability.Reader    as CR
import qualified Capability.Sink      as CSk
import qualified Capability.Source    as CSc
import qualified Capability.State     as CS
import qualified Capability.Writer    as CW
import qualified Control.Monad.Reader as MR
import qualified Control.Monad.State  as MS

import           Data.Bifunctor       (second)
import           Data.Monoid          (Sum (..))
import           GHC.Generics         (Generic)

------------------------------------------------------------

-- This is the generic action we will run.  The fact that the call to 'tell' is enclosed within
-- a call to 'local' should not affect the accumulated Writer value, but as we will see,
-- sometimes it does.
act :: (CW.HasWriter "w" (Sum Int) m, CR.HasReader "r" Char m) => m ()
act = CR.local @"r" (const 'b') $ do
  CW.tell @"w" (Sum 1)
  return ()

------------------------------------------------------------

-- One concrete monad with the required capabilities, simply using mtl transformers.
newtype M1 a = M1 { unM1 :: MS.StateT (Sum Int) (MR.Reader Char) a }
  deriving (Functor, Applicative, Monad)
  deriving (CW.HasWriter "w" (Sum Int), CSk.HasSink "w" (Sum Int)) via
    (CW.WriterLog
    (CS.MonadState
    (MS.StateT (Sum Int) (MR.Reader Char))))
  deriving (CR.HasReader "r" Char, CSc.HasSource "r" Char) via
    (CR.MonadReader
    (MS.StateT (Sum Int) (MR.Reader Char)))

runM1 :: M1 a -> (a, Sum Int)
runM1 = flip MR.runReader 'a' . flip MS.runStateT mempty . unM1

------------------------------------------------------------

-- Another concrete monad with the required capabilities, this time using a state monad
-- with a single record, using the Field strategy to pick out fields for the various capabilities.

data S = S { w :: Sum Int, r :: Char }
  deriving (Eq, Ord, Show, Generic)

newtype M2 a = M2 { unM2 :: MS.State S a }
  deriving (Functor, Applicative, Monad)
  deriving (CW.HasWriter "w" (Sum Int), CSk.HasSink "w" (Sum Int)) via
    (CW.WriterLog
    (CS.Field "w" ()
    (CS.MonadState
    (MS.State S))))
  deriving (CR.HasReader "r" Char, CSc.HasSource "r" Char) via
    (CS.Field "r" ()
    (CR.ReadStatePure
    (CS.MonadState
    (MS.State S))))

runM2 :: M2 a -> (a, Sum Int)
runM2 = second w . flip MS.runState (S 0 'a') . unM2

------------------------------------------------------------

main = do
  let ((), s') = runM1 act
  print (getSum s')

  let ((), s) = runM2 act
  print (getSum s)

Expected behavior
I expect the above code to print 1 twice; which concrete monad + deriving strategies we use should not change the semantics of 'act', especially when only Reader + Writer are involved (which should commute etc.) and there are no IO or exceptions anywhere to be seen.

Instead, the above code prints 1, then 0.

Environment

  • OS name + version: Ubuntu 20.10
  • Version of the code: I am using capability 0.4.0.0 and compiling with GHC 8.10.4 and cabal-install 3.4.0.0.

Deriving tagged instances requires `PolyKinds`

Deriving tagged instances requires the PolyKinds or TypeInType extension to be enabled. E.g. the following code

newtype MyState a = MyState (State Int a)
  deriving (Functor, Applicative, Monad)
  deriving (HasState "foo" Int) via
    MonadState (State Int)

raises compiler errors of the form

    Illegal kind: ([] :: [] ghc-prim-0.5.3:GHC.Types.RuntimeRep)
    Did you mean to enable PolyKinds?

This is likely due to the use of Proxy# in HasState's methods. #15073 might be related.

Having to enable PolyKinds can be a nuisance, because it may require the user to provide additional kind signatures in the affected module. See #4 (comment).

Associate type to tags

Currently, we have to write

HasState "mytag" MyType m =>

This is all fine for library, and basically necessary for deriving-via combinators. But it's a bit annoying in an application. Where typically, a tag correspond to a given type. e.g. HasReader "config" would always be followed by the same configuration type. Throughout the entire application. Meh 🙁 .

We should do something about this.

At this point my idea looks a bit as follows. We define a type family

type familly TypeOf (s :: Symbol) :: *

(with no instance in the library)

And type synonyms like

type HasState' s = HasState s (TypeOf a)

This way, you could define application-wide tag-to-type associations and have shorthands for them.

Or maybe, in order to avoid the lack of composability that symbol have, we could do

type family TypeOf (s :: *) :: *

That way they can be defined without orphan instances.

Thoughts?

Elimination methods in classes should be moved

Methods like local, catch, listen, etc, are awkward holdovers from mtl. They are effect handlers, in that they operationaly eliminate the main effect of the class (reading, throwing, writing, etc.). This is alright in mtl's intensional model (to use the blog post's term), but out of place in capability's extensional model.

The downsides see practical, not just philosophical. Firstly, It's weird that there is no way to hand out the capability without also handing out the addition capability to intercept the former and implement it another way. There is no technical reason to be so coarse grained. Intuitively, handling an effect is a more privileged operation: I can raise a complaint to the cops or HR, but only they can dismiss it (throw, catch). I can forward packets and mail but I shouldn't modify them (ask, local). I can flush the toilet in my apartment but cannot receive my neighbors sewage (tell, listen). MTL makes me citizen-cop, mailman-censor, tenent-plumber, but never just the first of each pair.

Secondarily, it's unfortunate that the types hide that the effect is in fact eliminated (a catch application needs the original HasThrow, a local application needs the original HasReader, etc.). However things may happen operationaly, at the type level effects/constraints are only accumulated, until the final grand discharge in main.

In an ideal world, I'd rip these methods out and indeed replace them with some other mechanism to prove the elimination of the effect, like running the underlying transformer. I don't see how to do that in all cases though without incurring other downsides, so instead I advocate the simpler approch of splitting the classes: Just as MonadError is spilit into MonadThrow and MonadCatch, so split the others to separate the introduction of each effect and it's elimination. At least this solves the first problem, and isolates the second. If/when a better way of handling effects comes along, code that only introduces effects (throws/asks/etc.) can stay the same.

Don't export class methods by default

The methods of the capability type-classes (e.g. put_ from HasState) are not meant to be used directly, currently this is documented in their haddocks. With the separation of user facing modules and internal modules, such as Capability.State and Capability.State.Internal.Class, it is possible to not export them from the user facing modules.

We should consistently separate user facing from internal modules (e.g. Capability.Writer vs Capability.Writer.Internal.Class) and not export class methods in the user facing modules.


cc @mrkkrp

Push haddocks to S3 from CI and link from README

Currently, the README only links to the CI dashboard and users have to click their way to the generated haddocks. We should provide an S3 bucket and automatically push haddock artifacts there on successful CI builds on master. The README should then link to the haddocks on S3.

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.