-
Notifications
You must be signed in to change notification settings - Fork 7
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
WIP: sketch out a reference API #417
base: main
Are you sure you want to change the base?
Conversation
d01ed4e
to
30aa13a
Compare
The focus is on a Ref to an object not an object with a RefCounter. Each Ref must be released exactly once. This is easier to dynamically check in debug builds, compared to the existing API which simply increments and decrements reference counts. This commit just adds the new API, leaving the existing API as well.
30aa13a
to
0f240ba
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The following are just a few comments that came up while scanning the PR. I'll let @mheinzel and you discuss the details of the approach, if that's okay
Overall, I think the sketch could benefit from a small example (with instances for HasFinaliser
and HasSharedFinaliser
), so that it is clear how all the moving parts interact
assertNotReleased released | ||
unsharedFinaliser obj | ||
unsafeIOToPrim $ writeIORef released True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should care about atomicity/exception safety here, and in other places in the module. Is it fine to do a simple check beforehand, or should we check that it is not released during the run of the finaliser? If an async exception happens, is it fine if we have run the finaliser but haven't updated the IORef
yet?
Regardless of what approach we pick, we should probably comment on the guarantees somewhere in this module
unsafeDeRef (Ref obj) = obj | ||
#else | ||
unsafeDeRef (Ref obj released) = | ||
unsafeDupablePerformIO (assertNotReleased released) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you explain why we use the dupable
variant here? Just asking because I'm not too familiar with when to use which unsafePerformIO
variant
#endif | ||
|
||
-- | If the object is still alive, obtain a /new/ normal reference. The normal | ||
-- rules for 'Ref' apply, including the need to eventually calling 'releaseRef'. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-- rules for 'Ref' apply, including the need to eventually calling 'releaseRef'. | |
-- rules for 'Ref' apply, including the need to eventually call 'releaseRef'. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks nice!
The debugging/assertion functionality could still be improved, I think. Would an error from assertNotReleased
currently tell you much about which (type of) resource wasn't released? Also, we don't find out yet if some Ref
just never gets closed, right? For that we'd need some global state or rely on GHC finalisers, as we discussed at some point.
The API seems sensible, though, which I think is the main point of the PR.
I also left some comments on minor details.
#endif | ||
|
||
#ifdef NO_IGNORE_ASSERTS | ||
assertNotReleased :: PrimMonad m => IORef Bool -> m () |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want a HasCallStack
constraint here?
-- | ||
class HasFinaliser obj where | ||
type FinaliserM obj :: Type -> Type | ||
unsharedFinaliser :: FinaliserM obj ~ m => obj -> m () |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was there some complication that led you to using a type family and equality constraints, as opposed to HasFinaliser m obj
, potentially with a functional dependency?
#ifndef NO_IGNORE_ASSERTS | ||
withRef :: Ref obj -> (obj -> m a) -> m a | ||
withRef (Ref obj) f = f obj | ||
#else | ||
withRef :: PrimMonad m => Ref obj -> (obj -> m a) -> m a | ||
withRef (Ref obj released) f = assertNotReleased released | ||
>> f obj | ||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be nicer to have the same type signature in both cases, so we can't run into unused constraint warnings at the usage sites that only show up in non-debug builds. This means we'd have to deal with the redundant constraint issue here, but it's probably the right place.
released' <- unsafeIOToPrim $ newIORef False | ||
return (Ref obj released') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part is just newRef
, which even already does the CPP, so potentially only assertNotReleased
would need to be conditional (but only CPP-ing half of the function might not make the code clearer).
#ifndef NO_IGNORE_ASSERTS | ||
deRefWeak (WeakRef obj) = do | ||
success <- tryIncrementRefCounter (getRefCounter obj) | ||
if success then return (Just (Ref obj)) | ||
else return Nothing | ||
#else | ||
deRefWeak (WeakRef obj) = do | ||
success <- tryIncrementRefCounter (getRefCounter obj) | ||
if success | ||
then do released <- unsafeIOToPrim $ newIORef False | ||
return (Just (Ref obj released)) | ||
else return Nothing | ||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This CPP could all go away using newRef
, though, right?
As opposed to a referencing counting API.