Polysemy: Design heuristics: Hiding vs Exposing
When it comes to interpreters relying on other effects, you can either hide or expose them.
For example, last year we introduced a Cache effect:
data Cache k v (m :: Type -> Type) a where
Cached :: k -> Cache k v m v
makeSem ''Cache
runCache f =
evalState @(M.Map k v) mempty
. reinterpret
( \case
Cached k -> do
currentCache <- get @(M.Map k v)
case currentCache M.!? k of
Nothing -> do
v <- raise $ f k
put $ M.insert k v currentCache
return v
Just v -> return v
)
The main motivation are:
- We don't want another interpreter to interact on it, breaking effect's logic
State
is only used locally
Note that you may want to expose it:
- If you want to test it, but exposing
State
will create interpretation-coupled tests (brittle), you can leverage interceptors instead - If you want to add more operation (for example a
Purge
operation in another effect), you can useInterpretersFor
On another hand, you have effects relying on other monads, such as a Document relying on BloodHound, or multiples effects relying on a single one:
data DocumentEffect d (m :: Type -> Type) a where
CreateDocument :: d -> DocumentEffect d m Id
UpdateDocument :: Id -> (d -> d) -> DocumentEffect d m ()
makeSem ''DocumentEffect
interpreterBH index =
interpret $
\case
CreateDocument doc -> embed @BH $ BH.indexDocument index doc
UpdateDocument docId f -> embed @BH $ BH.updateDocument index docId f
See the full the code here.