-
Notifications
You must be signed in to change notification settings - Fork 65
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
Add failing test for purescript-contrib/purescript-aff#174 #175
base: main
Are you sure you want to change the base?
Conversation
This is a tough call, and I think this is actually a problem with |
Alright, to be honest, I think I may be out of my depth there, I was hoping it might be a small fix on top of the existing bracketing code. Been in Aff.js for good chunk of time last night, and it's incredible difficult to trace all the mutations and how the slots relate to the current tag, etc. |
Fix purescript-contrib#174 by guaranteeing that iff a bracket body handler completes uninterrupted (e.g. if masked, or simply no interrupts,) it *will* call the 'completed' handler of the surrounding bracket. This effectively allows writing code like this: ```purescript bracket (liftEffect $ Ref.new Nothing) { completed: \a ref -> liftEffect $ Ref.write (Just a) ref , killed: \_ _ -> ... , failed: \_ _ -> ... } \_ -> action ``` We can effectively reason about the fact that, should `action` ever complete, we *will* run the completion handler. Currently, if `action` is masked, and thus guaranteed to succeed, we invoke the 'killed' handler, thus denying us the possibily of obtaining 'a', even if it was produced.
Here's a motivating example for this change. Previously a withLock
:: forall a
. Aff a
-> AVar Unit
-> Aff a A possible implementation could look like this: withLock action lock =
bracket (AVar.take lock)
(\_ -> AVar.put lock)
(\_ -> action) However, this implementation means that that the entire Another possible implementation could then look like this: withLock action lock =
bracket (pure unit)
(\_ -> Avar.put lock)
(\_ -> do
AVar.take lock
action
) However, it is now impossible to discern, at time of cancellation, whether or This pull request enables the following function to be written: atomic :: (v -> Aff Unit) -> Aff v -> Aff v
atomic postFn action =
generalBracket (pure unit)
{ completed: \v _ -> postFn v
, killed: \_ _ -> pure unit
, failed: \_ _ -> pure unit
} action It is guaranteed that iff Using this function, we can now implement withLock action lock =
bracket (liftEffect (Ref.new false))
(\ref ->
liftEffect (Ref.read ref) >>= \hasTakenLock ->
when hasTakenLock $ Avar.put lock)
(\ref ->
atomic (\_ -> liftEffect $ Ref.write true ref) $ AVar.take lock
action
) |
On further thought, my motivating example is flawed. An interrupt during So, really the motivating example is a bad one. A better one, I believe, would be the following: module Test.Main where
import Prelude
import Data.Maybe (Maybe(..))
import Data.Time.Duration (Milliseconds(..))
import Effect (Effect)
import Effect.Aff (Aff, delay, error, forkAff, invincible, killFiber, launchAff_)
import Effect.Class (liftEffect)
import Effect.Console as Console
import Effect.Ref (Ref)
import Effect.Ref as Ref
newtype LazyVal a =
LazyVal { ref :: Ref (Maybe a)
, acquireFn :: Aff a
}
newLazyVal :: ∀ a. Aff a -> Effect (LazyVal a)
newLazyVal acquireFn = ado
ref <- Ref.new Nothing
in LazyVal { ref, acquireFn }
acquire :: ∀ a. LazyVal a -> Aff a
acquire (LazyVal { ref, acquireFn }) = do
liftEffect (Ref.read ref) >>= case _ of
Just v ->
pure v
Nothing -> do
v <- acquireFn
v <$ liftEffect (Ref.write (Just v) ref)
newLeakedLazyVal :: Effect (LazyVal String)
newLeakedLazyVal =
newLazyVal $
invincible $
"hello" <$ do
delay (10.0 # Milliseconds)
liftEffect $ Console.log "yielded!"
main :: Effect Unit
main = launchAff_ do
lv <- liftEffect newLeakedLazyVal
f1 <- forkAff $ acquire lv
delay (5.0 # Milliseconds)
killFiber (error "abort!") f1
acquire lv The idea is to capture a shared resource that can be lazily acquired. Since the acquistion function is just any old If you run the above example, you will see "yielded!" printed twice to the console. The resource is never captured in the To be clear, however, I am not proposing a change to purescript-aff that would make the above work without changes. The semantics are correct, as far as I am concerned that if an interrupt happens during a partially evaluated bracket body, it should of course be treated as killed. However, if the body did fully evaluate (e.g. it was interrupted while masked), it should call the completed handler. The acquire :: ∀ a. LazyVal a -> Aff a
acquire (LazyVal { ref, acquireFn }) = do
liftEffect (Ref.read ref) >>= case _ of
Just v ->
pure v
Nothing ->
generalBracket (pure unit)
{ completed: \v _ -> liftEffect (Ref.write (Just v) ref)
, killed: \_ _ -> pure unit
, failed: \_ _ -> pure unit
} (const acquireFn) |
I'll also make an attempt at fixing this, but if it goes nowhere, feel free to copy the test code.