-
Notifications
You must be signed in to change notification settings - Fork 35
Add Effectful.ST and Effectful.ST.STRef #339
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Thanks for the PR 👍 However... Did you see https://hackage-content.haskell.org/package/effectful-core-2.6.0.0/docs/Effectful-Prim.html? It allows you to use everything from I considered making it For example code like this: won't compile without In conclusion: I experimented with this approach and decided it's not worth it over how currently |
I have! The main use case I see for For example, what motivated me to make this PR was a very generic graph algorithm of type (essentially) computeSCC :: (Hashable node) => (node -> Eff es [node]) -> node -> Eff es (HashMap node SCCId)The function to access a node's edges needs to run in an effect since edges sometimes need to be computed from a mutable structure, so I can't just use regular As for the ambiguity, I agree that it is a bit unfortunate, but I don't think it's as bad as it seems. A function using So an example like your blub :: Eff es ()
blub = runSTE \(Proxy :: Proxy s) -> do
_ <- newSTRef @s 'x'
pure ()Moreover, the only operations that ever need to use the So really, the only operations that need type applications are operations like |
|
As a concrete example, the SCC algorithm I mentioned works with (simplified to reduce dependencies so i could check it in the effectful repo)computeSCC ::
forall node es.
(Show node, Ord node) =>
(node -> Eff es (Maybe [node])) ->
node ->
Eff es (Map node Int)
computeSCC outEdgesOrPrecomputedSCC node = runSTE $ \(Proxy :: Proxy s) -> do
visited :: STRef s (Set node) <- newSTRef [node]
currentDFSNum :: STRef s Int <- newSTRef 0
dfsNums :: STRef s (Map node Int) <- newSTRef mempty
openSCCs :: STRef s [node] <- newSTRef [node]
openNodes :: STRef s [node] <- newSTRef [node]
sccs :: STRef s (Map node Int) <- newSTRef mempty
sccIds :: STRef s Int <- newSTRef 0
let newSCCId = do
id <- readSTRef sccIds
writeSTRef sccIds (id + 1)
pure id
let go node = do
dfsNum <- readSTRef currentDFSNum
writeSTRef currentDFSNum (dfsNum + 1)
modifySTRef' dfsNums (Map.insert node dfsNum)
raise (outEdgesOrPrecomputedSCC node) >>= \case
Nothing -> pure ()
Just neighbors -> do
for_ neighbors $ \neighbor -> do
visitedUntilNow <- readSTRef visited
case Set.member neighbor visitedUntilNow of
False -> do
modifySTRef' visited (Set.insert neighbor)
modifySTRef' openSCCs (neighbor :)
modifySTRef' openNodes (neighbor :)
go neighbor
True -> do
dfsNumsUntilNow <- readSTRef dfsNums
let dfsNumOf otherNode = case Map.lookup otherNode dfsNumsUntilNow of
Nothing -> error $ "DFS number of '" <> show node <> "' not found"
Just dfsNum -> dfsNum
modifySTRef' openSCCs (dropWhile (\representative -> dfsNumOf representative >= dfsNum))
-- backtrack
readSTRef openSCCs >>= \case
(topRepresentative : rest)
| topRepresentative == node -> do
writeSTRef openSCCs rest
sccId <- newSCCId
let assignSCC = \case
[] -> pure []
(openNode : rest) -> do
modifySTRef' sccs (Map.insert openNode sccId)
if openNode == node
then pure rest
else assignSCC rest
currentOpenNodes <- readSTRef openNodes
remainingOpenNodes <- assignSCC currentOpenNodes
writeSTRef openNodes remainingOpenNodes
_ -> pure ()
go node
readSTRef sccs
|
It's a bit different, because my example is a util function for where STE is already in scope, while yours introduces and discharges it. Ok, I see STE has an advantage over Prim in that you can introduce it anywhere in Eff without IOE in scope. But somewhat awkward usability without a plugin and multiple designs (Proxy vs no Proxy) remains. Also, there's already https://hackage.haskell.org/package/effectful-st, but it takes a slightly different route and provides just adapter functions. Perhaps it would be worth coordinating with its author and introduce this PR there? Btw, looking at this package I just remebered about #93, so this discussion happened before :) And the author of the ticket is the author of effectful-st. Looks like I remembered wrong in that Prim can't be parametrized with |
With this, effectful is able to mix
STEwith other effects, which makes it a very convenient way to avoid having to depend onIOEjust to locally use mutable data structures.The effect is tagged with an
sregion parameter andrunSTEuses the same higher rank type trick as regularrunSTto prevent references from escaping.I'm pretty sure that this is sound, however
STcan be subtle so it would be nice to have a second opinion.For some prior art: Koka uses a similar
steffect that can be combined with arbitrary other effects.References escaping through other effects (e.g. exceptions) shouldn't be an issue since that can only happen through an existential, at which point the reference cannot be accessed anymore anyway.
One thing I'm not entirely sure about is the type role on
STE. The region parameter needs to be nominal for soundness, but making the othersphantomseems a bit dangerous? This is also what is inferred for other effects likeProcessthough.