haskellmonadsmonad-transformersreader-monadst-monad

Modify ST dependent environment in ReaderT – problem with `local` function


This question is a sequel of this thread: https://stackoverflow.com/a/54317095/4400060

I was asking there about carrying STRef in ReaderT's environment and performing ST-actions under it. My setup now looks like:

import Data.HashTable.ST.Cuckoo as HT

-- |Environment for Comp
newtype Env s = Env { dataspace :: HashTable s Int Data
                    , namespace :: Map Name Int }

-- |Main computation monad
newtype Comp a = Comp (forall s. ReaderT (Env s) (ST s) a)


-- |Evaluate computation
runComp (Comp c) = runST $ do
    ds <- HT.new
    runReaderT c (Env ds empty)


-- |Perform an action on `dataspace` hashmap
onDataspace :: (forall s. HashTable s Int Data -> ST s a) -> Comp a
onDataspace f = Comp $ asks dataspace >>= lift . f

And it works cool in general – I can access or modify dataspace in place freely. However, when I have added immutable namespace I started to struggle. Feature I need is running Comp action with updated namespace in the way that it won't affect further computations' namespaces – exactly what local does.

First of all I wanted to write MonadReader instance for Comp, however I faced the ST's phantom type and got illegal instance error:

instance MonadReader (Env s) Comp where {}
instance MonadReader (forall s. Env s) Comp where {}
instance forall s. MonadReader (Env s) Comp where {}

Full error message:

Illegal instance declaration for
     ‘MonadReader (EvalEnv s) Evaluator’
     The coverage condition fails in class ‘MonadReader’
       for functional dependency: ‘m -> r’
     Reason: lhs type ‘Evaluator’
       does not determine rhs type ‘EvalEnv s’
     Un-determined variable: s

I understand this error, but I see no way bypassing it. To be honest I don't really require full local function. I only need to be able to run Comp with different namespace, but same dataspace.

The best solution would be to provide full MonadReader instance. I am aware that it might not be possible, so as a workaround I would like to have a function

withNs :: Map Name Int -> Comp a -> Comp a

Summarizing: I want to be able to run Comp with modified namespace while leaving dataspace unchanged as a reference keeping all changes under it.

How to do that? I can accept modifying my initial setup if needed.


Solution

  • The scope parameter s of ST should remain outside:

    newtype Comp s a = Comp (ReaderT (Env s) (ST s) a)
    

    The only place where you need a higher-rank type is when calling runST.

    runComp :: (forall s. Comp s a) -> a
    runComp = runST $ do
      ds <- HT.new
      runReaderT c (Env ds empty)
    

    Everywhere else you can simply be parametric in s.

    doStuff :: Comp s Bool
    doMoreStuff :: Comp s Int
    

    Then the MonadReader instance can be written:

    instance MonadReader (Env s) (Comp s) where
      ...