Transformer version of the ST monad?

I want to stack the ST Monad together with other conveniences like MonadError, and so on.

It seems that there are packages like STT, but they are widely used, or their use is discouraged.

What is the best way to write a convenient computation with other effects, like MonadError and so on, such that runST can still be run on it later?

Best way is subjective, but if you don’t mind ending up in IO, you can use effectful - apart from standard stuff like Error, State etc. it has a Prim effect that gives you access to everything in the primitive library, which is an extension of ST pretty much.

Alternatively you can use ExceptT on top of ST, but then you’ll have to write lift a lot.

2 Likes

That primitive library also has a MonadPrim class with instances for ST.

3 Likes

This is an interesting option, but I would prefer not to end up in IO. This is why I am looking to combine something like runST with other effects (like throwing errors, for example).

Can we define an effect in effectful which utilizes the same trick as ST which allows the result to be pure? Perhaps something like this:

data EffectST s :: Effect where
  LiftST :: ST s a -> EffectST s m a

runEffectST :: (forall s. Eff (EffectST s : es) a) -> Eff es a

I don’t think you can, as STTrans [] would have to duplicate the STRef in memory so that the list refrences wouldn’t be mutated by each other.

I would recommend transforming ST instead of looking for a monad transformer that gives you ST effects. The latter would not behave the way you want for all monads.

yeah, this is probably what I am looking for. The only annoying boilerplate here is the liftST.


type BankM s a = ExceptT Text (ReaderT (STRef s Int) (ST s)) a

liftST :: ST s a -> BankM s a
liftST = lift . lift

withdraw :: Int -> BankM s ()
withdraw amount = do
  ref <- ask
  currentBalance <- liftST $ readSTRef ref
  if amount > currentBalance
    then
      throwError $ "Insufficient funds! Balance is: " <> Text.pack (show currentBalance)
    else
      liftST $ writeSTRef ref (currentBalance - amount)

runBank :: Int -> (forall s. BankM s a) -> Either Text a
runBank initialBalance action = runST $ do
  ref <- newSTRef initialBalance
  runReaderT (runExceptT action) ref

getBalance :: BankM s Int
getBalance = do
  ref <- ask
  liftST $ readSTRef ref

pureWithdraw :: Int -> Either Text Int
pureWithdraw amount = runBank 1000 $ do
  withdraw amount
  getBalance

Bluefin basically is “ST-with-more-effects”.

The Bluefin introduction has example3 which uses a state reference and an exception and ends up in pure code, not IO.

example3 :: Int -> Either String Int
example3 n = runPureEff $
  try $ \ex -> do
    evalState 0 $ \total -> do
      for_ [1..n] $ \i -> do
         soFar <- get total
         when (soFar > 20) $ do
           throw ex ("Became too big: " ++ show soFar)
         put total (soFar + i)

      get total

Check out

Let me know if you have any more questions. You’re welcome to message me here, by opening a new issue on the Bluefin repo or, if necessary, by email.

1 Like

Thanks for the pointer. Indeed, looking at the type signature

runState ::
  forall s (es :: Effects) a. s
  -> (forall (e :: Effects). State s e -> Eff (e :& es) a)
  -> Eff es (a, s)

it seems that it also has a similar trick going on.

Can one use something like this ST based mutable Hashtable in Bluefin?

Yes exactly, it’s the ST-trick but the es type variable pokes out so you can use it for other effects.

EDIT: yes, in a work in progress branch. For example, this mixes ST and IO. Of course you want to not use IO. That’s fine, it could be any other effects, or none. I just wanted to be able to print stuff out!

Please do file a new issue on Bluefin and encourage me to merge this if you’re interested in using it.

-- ghci> example
-- 0
-- 10
-- <HashTable>
-- [("hello",42),("bye",1099)]
-- No more ST
example :: IO ()
example = runEff_ $ \io -> do
  do
    runST $ \st -> do
      ref <- effST st (STRef.newSTRef @Int 0)
      v1 <- effST st (STRef.readSTRef ref)
      effIO io (print v1)
      effST st (STRef.modifySTRef ref (+ 10))
      v2 <- effST st (STRef.readSTRef ref)
      effIO io (print v2)
      ht <- effST st (HT.new @_ @String @Int)
      effST st (HT.insert ht "hello" 42)
      effST st (HT.insert ht "bye" 99)
      effIO io (print ht)
      effST st (HT.mutate ht "bye" $ \m -> (fmap (+ 1000) m, ()))
      htList <- effST st (toList ht)
      effIO io (print htList)
  effIO io (putStrLn "No more ST")```

EDIT: Old:

Yes, but I don’t have a generic way of running ST actions in Bluefin yet, so I think the simplest way is just to wrap the IO version like

newtype BfHashTable k v es =
  MkBfHashTable (IO.IOHashTable HashTable k v)
new ::
  (forall e. BfHashTable k v e -> Eff (e :& es) r) ->
  Eff es r
new = ... IO.new
lookup ::
  (Eq k, Hashable k, e :> es) =>
  BfHashTable k v e ->
  k ->
  Eff (Maybe v) 
lookup = ... IO.lookup

You can follow the recipe that is used by State to wrap IORef and get an equivalent of STRef: bluefin/bluefin-internal/src/Bluefin/Internal.hs at 21994b19fd90db550353da4132db7580ea8befec · tomjaguarpaw/bluefin · GitHub

This is all a bit complicated to implement but I’m happy to help, and once it’s implemented it should be as easy as ST to use.

2 Likes