haskell / core-libraries-committee

96 stars 15 forks source link

Deprecate `stToIO` and introduce `safeSTToIO` #119

Closed noughtmare closed 1 year ago

noughtmare commented 1 year ago

Summary

The existence of the safe stToIO function enables one to use ST computations in multiple threads. This means we technically don't have the guarantee that ST computations run in a single thread which most people expect and base their programs on. I propose address this by deprecating stToIO, introducing one safe but more restricted replacement and one function which does the same thing but is explicitly marked unsafe, and adding documentation about this issue.

Example

We might think ST computations are single-threaded an thus do not need to use atomic operations (in fact, STRef has no atomic modify operation), so we might write a programST function as follows:

programST :: STRef s Integer -> ST s ()
programST ref = do
  n <- readSTRef ref
  if n <= 0
    then pure ()
    else do
      unsafeIOToST yield
      writeSTRef ref $! n - 1
      programST ref

(The unsafeIOToST yield is only to make it more likely that weird concurrent interleavings occur)

But we can actually use this function in an unsafe way:

countdownST :: Integer -> IO Integer
countdownST n = do
  ref <- stToIO (newSTRef n)
  forkIO $ stToIO (programST ref)
  stToIO (programST ref)
  s <- stToIO (readSTRef ref)
  pure s

main :: IO ()
main = do
  putStrLn . show =<< countdownST 1000000

Compiling with ghc -O2 T.hs -threaded and running with ./T +RTS -N2 sometimes gives me unexpected results:

$ ./T +RTS -N2
129

$ ./T +RTS -N2
100
Click to expand full repro source code ```haskell import Control.Monad.ST import Control.Monad.ST.Unsafe import Control.Concurrent import Data.STRef programST :: STRef s Integer -> ST s () programST ref = do n <- readSTRef ref if n <= 0 then pure () else do unsafeIOToST yield writeSTRef ref $! n - 1 programST ref countdownST :: Integer -> IO Integer countdownST n = do ref <- stToIO (newSTRef n) forkIO $ stToIO (programST ref) stToIO (programST ref) s <- stToIO (readSTRef ref) pure s main :: IO () main = do putStrLn . show =<< countdownST 1000000 ```

Proposed changes

I propose to introduce two new functions:

safeSTToIO :: (forall s. ST s a) -> IO a
safeSTToIO (ST f) = IO f

unsafeRealSTToIO :: ST RealWorld a -> IO a
unsafeRealSTToIO (ST f) = IO f

Additionally, I propose to deprecate (but not remove) stToIO suggesting users to use the new safeSTToIO or unsafeSTToIO instead.

I do not propose removing stToIO because that would break too many existing packages. Perhaps we can consider that at a later time, when most of the ecosystem has adapted to this change.

Addtionally, documentation should be updated to explain the unsafety.

Impact

The results from this hackage search shows that there are 674 matches of the string stToIO in 95 packages. Some matches may be in comments, using the more complicated pattern stToIO\s*[$(]|=\s*stToIO in an attempt to exclude occurrences in comments yields 515 matches across 71 packages.

Adding a deprecation warning lets package maintainers update their packages at their own pace, so I expect the migration to be a smooth process.

See also

https://gitlab.haskell.org/ghc/ghc/-/issues/22780 https://gitlab.haskell.org/ghc/ghc/-/issues/22764#note_473050

treeowl commented 1 year ago

safeStToIO appears to be equivalent to pure $! runST k, which is more general - Applicative f => (forall s. ST s a) -> f a.

As I explained way up thread, this is incorrect.

parsonsmatt commented 1 year ago

Is that due to the semantics of pure () >> runST (pure @IO <$> st) or the optimization difficulty?

treeowl commented 1 year ago

The semantics. forall s. ST s a is closer to Solo a than to a. STtoIO reveals that. runST confuses matters by throwing away the distinction. I think if the system were being designed today, we'd likely have runST :: (forall s. ST s a) -> Solo a.

parsonsmatt commented 1 year ago

That makes sense, thank you for clarifying!

Bodigrim commented 1 year ago

@noughtmare could you please raise a draft MR, which we can vote on?

@noughtmare just a gentle reminder to make some progress. Otherwise I'll close the proposal as abandoned in two weeks.

Bodigrim commented 1 year ago

Closing as abandoned.

treeowl commented 1 year ago

I'd really like to get the safe version, with or without any deprecation. Do I need to open my own issue?

Bodigrim commented 1 year ago

@treeowl would you like to take over the proposal? I think it's pretty much ready as is, except a missing MR.