I was wondering if effectful has a policy on the thread safety of code written using it. Currently the provided functionality seems to support
make it easier to write thread-safe code than thread-unsafe code
(which seems perfectly reasonable). I wonder if this is actually the intended policy. In particular, it's quite easy to write code that doesn't look obviously thread-unsafe, but actually is. For example, something like the below.
The reason this isn't obviously thread unsafe is that the use of Effectful.State.Dynamic in useStateConcurrently and its interpretation as an IORef in evalState can be very far apart in a codebase. That is, although it's obvious at the definition site of useStateConcurrently that State Int must not be handled by a thread-unsafe handler, and although it's obvious at the definition site of evalState that it must not be used to handle an effect that will be used concurrently, if those two definitions are far apart then we have to keep track of their incompatibility manually, rather than having the type system guide us.
In particular, it is possible to interpret any dynamic effect, even one that might be used concurrently, in a non-thread safe way.
As far as I can tell, effectful's policy is essentially "Use either Effectful.State.Local or Effecful.State.Dynamic whenever a state effect might be used concurrently", and effectful's "copy on fork" makes this very convenient in practice, hence "make it easier to write thread-safe code than thread-unsafe code". But it's still easy to get it wrong. One could imagine a more restrictive type system that doesn't allow this kind of sharing between threads, thus my question: is there a particular policy?
#!/usr/bin/env cabal
{- cabal:
build-depends: base, effectful==2.5.1.0, async
-}
{-# LANGUAGE GHC2021 #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
import Control.Concurrent
import Control.Concurrent.Async
import Data.IORef
import Effectful
import Effectful.Dispatch.Dynamic
import Effectful.State.Dynamic
evalState ::
(IOE :> es) =>
s ->
Eff (State s : es) a ->
Eff es a
evalState s0 m = do
v <- liftIO (newIORef s0)
reinterpret id (ioState v) m
ioState ::
(IOE :> es) =>
IORef s ->
LocalEnv localEs es ->
State s (Eff localEs) a ->
Eff es a
ioState v env = \case
Get -> liftIO (readIORef v)
Put s -> liftIO (writeIORef v s)
State f -> liftIO $ do
s <- readIORef v
let (r, s') = f s
writeIORef v s
pure r
StateM _ -> error "Dunno"
useStateConcurrently ::
(State Int :> es, IOE :> es) => Eff es ()
useStateConcurrently = do
withEffToIO (ConcUnlift Persistent Unlimited) $
\effToIO -> do
concurrently
( effToIO $ do
liftIO (threadDelay 500)
s <- get @Int
put (s + 1)
)
( effToIO $ do
s <- get @Int
liftIO (threadDelay 1000)
put (s * 2)
)
(liftIO . print) =<< get @Int
-- We "want" the result to be either
--
-- - 12 (== (5 + 1) * 2), or
--
-- - 11 (== (5 * 2) + 1)
--
-- but we get
--
-- % cabal run test-effectful-thread-unsafe.hs
-- 10
main :: IO ()
main = runEff $ do
evalState @_ @Int 5 $ do
useStateConcurrently
I was wondering if effectful has a policy on the thread safety of code written using it. Currently the provided functionality seems to support
(which seems perfectly reasonable). I wonder if this is actually the intended policy. In particular, it's quite easy to write code that doesn't look obviously thread-unsafe, but actually is. For example, something like the below.
The reason this isn't obviously thread unsafe is that the use of
Effectful.State.DynamicinuseStateConcurrentlyand its interpretation as anIORefinevalStatecan be very far apart in a codebase. That is, although it's obvious at the definition site ofuseStateConcurrentlythatState Intmust not be handled by a thread-unsafe handler, and although it's obvious at the definition site ofevalStatethat it must not be used to handle an effect that will be used concurrently, if those two definitions are far apart then we have to keep track of their incompatibility manually, rather than having the type system guide us.In particular, it is possible to interpret any dynamic effect, even one that might be used concurrently, in a non-thread safe way.
As far as I can tell, effectful's policy is essentially "Use either
Effectful.State.LocalorEffecful.State.Dynamicwhenever a state effect might be used concurrently", and effectful's "copy on fork" makes this very convenient in practice, hence "make it easier to write thread-safe code than thread-unsafe code". But it's still easy to get it wrong. One could imagine a more restrictive type system that doesn't allow this kind of sharing between threads, thus my question: is there a particular policy?