@@ -4,14 +4,29 @@ module Theseus.Effect.Reader (
44 local ,
55 asks ,
66 runReader ,
7+ DReader (DAsk , DLocal ),
8+ dask ,
9+ dlocal ,
10+ dasks ,
11+ ReadStack ,
12+ runDReader ,
713) where
814
15+ import Data.Foldable
16+ import Data.Monoid
917import Theseus.Eff
18+ import Theseus.Effect.State
1019
1120-- # Reader
1221
1322-- The Reader effect is a good example of a higher order effect because it's
14- -- otherwise very simple.
23+ -- otherwise very simple. There are two readers in this module. The first is
24+ -- a lexically scoped reader. The second is a dynamically scoped reader. The
25+ -- first is simpler and more naturally fits into Theseus' architecture. The
26+ -- second is more complicated. Coming from first order effect systems like
27+ -- `freer-simple`, the lexical scoping is what you'd expect. Coming from IO
28+ -- based higher order effect systems like effectful, dynamic scoping is what
29+ -- you'd expect.
1530
1631-- | Computations that use `Reader r` can ask the environment for some `r`.
1732-- They cannot change the `r`, but that does not necessarily mean that two
@@ -58,3 +73,80 @@ localReader :: forall r lb ub es a. (lb Identity, Reader r :> es) => (r -> r) ->
5873localReader f = interpret \ sender -> \ case
5974 Ask -> pure . f <$> ask
6075 Local newF m -> pure $ sender @ (Reader r ) $ localReader (newF . f) m
76+
77+ -- ## Dynamic Reader
78+
79+ -- The default (and simpler) version of Reader is lexically scoped. Code
80+ -- directly inside of a `local` see the modified value, but anything not
81+ -- "syntactically" inside the `local` will not. For example, imagine you wanted
82+ -- to keep track of a stack of locations while logging. For example, you might
83+ -- use some code like `withCtx "doingThing" do action` and any logs emitted within
84+ -- `...` should include the extra context. You might expect the normal `Reader`
85+ -- effect to work for this. If `action = do callDb; log` then it the `log`
86+ -- after `callDb` will work correctly. If `callDb` is an effect and the
87+ -- implementation of `CallDb` calls `log`, it won't see the local context. It
88+ -- would only see the context where `CallDb` is being handled, not the context
89+ -- where `CallDb` is being sent.
90+ --
91+ -- In cases like that one, having a dynamically scoped Reader is extremely
92+ -- helpful. Theseus includes that as a separate `DReader` type
93+
94+ -- | The dynamically scoped Reader has the same laws as `Reader`, but
95+ -- modifications created by `DLocal` are visible within effect interpretations
96+ -- instead of only being visible at the outermost level.
97+ data DReader r m a where
98+ DAsk :: DReader r m r
99+ DLocal :: lb Identity => (r -> r ) -> Eff lb ub es a -> DReader r (Eff lb ub es ) a
100+
101+ -- | Returns the `DReader`'s constant value.
102+ dask :: DReader r :> es => Eff lb ub es r
103+ dask = send DAsk
104+
105+ -- | Modifies the `DReader`'s value within some limited scope.
106+ dlocal :: (lb Identity , DReader r :> es ) => (r -> r ) -> Eff lb ub es a -> Eff lb ub es a
107+ dlocal f action = send $ DLocal f action
108+
109+ -- | A convenience function equivalent to `fmap f dask`.
110+ dasks :: DReader r :> es => (r -> a ) -> Eff lb ub es a
111+ dasks f = fmap f dask
112+
113+ -- The implementation of `DReader` uses `State` to keep track of a stack of
114+ -- local modifications. It looks a lot like the implementation of Readers in IO
115+ -- based effect systems. It relies on Theseus' `finally` function to ensure the
116+ -- stack is modified correctly.
117+
118+ -- | Runs a `DReader r` effect and consumes the `State ReadStack` that it
119+ -- depends on. The `State` is not a private effect because it needs to be
120+ -- sendable by any functions that want to send `DAsk`. That means you should
121+ -- never use raise to hide the `State ReadStack` unless you are also hiding the
122+ -- `DReader` associated with it. Getting this wrong will trigger `error` calls.
123+ --
124+ -- Although it's public, you should not modify the `State ReadStack`. Because
125+ -- `ReadStack` doesn't export its constructor, you shouldn't be able to do much
126+ -- with it anyway.
127+ runDReader ::
128+ forall r lb ub es a .
129+ (lb Identity , lb (StateResult (ReadStack r ))) =>
130+ r ->
131+ Eff lb ub (DReader r : State (ReadStack r ) : es ) a ->
132+ Eff lb ub es a
133+ runDReader r =
134+ evalState mempty . interpret \ sender -> \ case
135+ DAsk -> pure . apply r <$> get
136+ DLocal f m -> do
137+ modify (push f)
138+ pure $ sender @ (State (ReadStack r )) $ m `finally` modify (pop @ r )
139+
140+ -- | An opaque type holding the stack of local modifications.
141+ newtype ReadStack r = ReadStack [Endo r ]
142+ deriving (Semigroup , Monoid )
143+
144+ push :: (r -> r ) -> ReadStack r -> ReadStack r
145+ push f (ReadStack rs) = ReadStack (Endo f : rs)
146+
147+ pop :: ReadStack r -> ReadStack r
148+ pop (ReadStack [] ) = error " The read stack was empty. It might have been replaced with an incompatible one."
149+ pop (ReadStack (_ : rs)) = ReadStack rs
150+
151+ apply :: r -> ReadStack r -> r
152+ apply r (ReadStack rs) = fold rs `appEndo` r
0 commit comments