Skip to content
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

generalBracket By Design Is Not Nestable #166

Closed
eric-corumdigital opened this issue Jan 29, 2019 · 9 comments
Closed

generalBracket By Design Is Not Nestable #166

eric-corumdigital opened this issue Jan 29, 2019 · 9 comments

Comments

@eric-corumdigital
Copy link
Contributor

eric-corumdigital commented Jan 29, 2019

I am possibly misunderstanding how generalBracket works but here is my analysis.

My situation is that I want to take an AVar, do some action on the value, then put a value to the AVar and return the action result. The invariant is that if the AVar was taken then a value must be put to it, and if the AVar was not taken then a value must not be put to it. Taking the AVar must be killable.

This first example makes no attempt at bracketing but demonstrates what should happen on the happy path.

transact ∷ ∀ a b. (a → Aff (Tuple a b)) → AVar a → Aff b
transact f v = do
  a ← AVar.take v
  Tuple a' b ← f x
  AVar.put a' v
  pure b

There are several places in which a failure or kill breaks the invariant. Adding brackets, first to deal with taking the AVar must be killable:

transact f v =
  generalBracket
  (pure unit)
  { killed: \_ _ → pure unit
  , failed: \_ _ → pure unit
  , completed: …
  }
  (\_ → AVar.take v) 

And now this is stuck. There is no way to return the result from completed, so there is no way to continue transact given that taking the AVar succeeded.

In other words, brackets are not nestable. Say they were, however, this is what could be done:

transact f v =
  generalBracket (pure unit)
  { killed: \_ _ → pure unit
  , failed: \_ _ → pure unit
  , completed: \a _ →
      generalBracket (pure unit)
      { killed: \_ _ → AVar.put a v
      , failed: \_ _ → AVar.put a v
      , completed: \(Tuple a' b) _ → invincible (AVar.put a' v $> b)
      }
      (f a)
  }
  (\_ → AVar.take v) 

This assumes the typing:

generalBracket ∷ ∀ a b c. Aff a → BracketConditions a b c → (a → Aff b) → Aff c

type BracketConditions a b c =
  { killed ∷ Error → a → Aff Unit
  , failed ∷ Error → a → Aff Unit
  , completed ∷ b → a → Aff c
  }

If there is another way to implement this program I just am not seeing it right now.

@natefaubion
Copy link
Collaborator

Can this be accomplished with an additional map?

snd <$> generalBracket foo
  { ...
  , completed \(Tuple a _) _ -> doSomething with a
  } bar

It's not fully clear to me what you are accomplishing by only running something in the completed stage, and ignoring the others.

@eric-corumdigital
Copy link
Contributor Author

eric-corumdigital commented Jan 29, 2019

I cannot "do something with a" that returns a value on the completed case. That is the problem. Also, I am not ignoring the other cases; I provided values for each.

@safareli
Copy link
Contributor

safareli commented Jan 29, 2019

shouldn't this work?

transact f v = do
  a <- generalBracket (pure unit)
    { killed: \_ _ → pure unit
    , failed: \_ _ → pure unit
    , completed: \_ _ → pure unit
    }
    (\_ → AVar.take v)
  generalBracket (pure unit)
    { killed: \_ _ → AVar.put a v
    , failed: \_ _ → AVar.put a v
    , completed: \(Tuple a' b) _ → invincible (AVar.put a' v)
    }
    (f a)

if take succeeds then you have a and immediately after that you start f a. (I suppose it can't be killed after first generalBracket is finished and before second one starts

@eric-corumdigital
Copy link
Contributor Author

eric-corumdigital commented Jan 29, 2019

My assumption is that a >>= b can be killed after a and before b. I assume the killed and failed branches cannot be killed, but I am not sure about the completed branch. In my idealised variant, the completed branch is killable.

If binds cannot be killed then that would be curious to me. When exactly could an Aff action be killed then?

Edit: upon further thinking, I now suspect binds are not killable. Aff actions themselves have Cancelers, which to me suggests each action can be killed rather than the binds between them. I would need confirmation of this though to know if your solution works @safareli.

@safareli
Copy link
Contributor

Also note that JS is single threaded so when some sync operation is being performed nothing else is done at the same time. so nothing can kill some Aff computation which is in "middle" of bind. AFAIK completed can't be killed as in completed you usually are doing some cleanup of resources obtained by first argument of bracket, i.e. in bracket last argument can be interrupted.

@eric-corumdigital
Copy link
Contributor Author

@safareli yes it could be killed during bind depending on how it is implemented. It could be implemented such as:

bind m f = Aff \ctx → do
  a ← m
  checkForKill ctx
  f a

Maybe this violates some law but speculatively I don't rule such a possibility out.

@natefaubion
Copy link
Collaborator

natefaubion commented Jan 29, 2019

The only thing generalBracket guarantees is that the allocation block and one of the finalizer conditions is executed. Both are implicitly invincible. It does not guarantee that once it is allocated, the body block will ever execute. Additionally Aff does not guarantee that once an asynchronous makeAff resolves, it immediately starts executing the next effect. It is certainly possible for an async action to resolve, and then be canceled while it is still in the scheduler queue.

If I understand you correctly you want to satisfy an interface like:

newtype AVarLock a = AVarLock (AVar a)
modify :: forall a b. AVarLock a -> (a -> Aff (Tuple a b)) -> Aff b

With the conditions that:

  • If the AVar value is acquired, it must put a value back.
  • If the inner block is killed, the original value is put back.
  • If the inner block completes, the new value is put back.

I don't think that you want to run the inner block within a completed callback because that means it cannot be killed. The bracket handlers are always invincible so that it can guarantee that the resource is released. So even if you could return a new value from completed, it wouldn't behave the way you want.

You can possibly solve it with more AVars and Fibers. I know it sounds hacky, but AVars exist to do arbitrary async coordination. I would try forking the work you want done, and use generalBracket as the proxy for managing that fiber, where state is coordinated through AVars.

@eric-corumdigital
Copy link
Contributor Author

eric-corumdigital commented Jan 29, 2019

@natefaubion yes you have intuited my example accurately.

If generalBracket is just about resource acquisition and releasing then perhaps it was never intended in the way I have conceptualised it. I thought it was to let one continue from an action by case analysis of how it completed.

For this easy example I can use the AVar status during resource releasing to determine if it was left empty or not. It is a particular workaround only for this one example. Edit: Actually it depends on whether a kill can happen between the last action in a generalBracket and the completion handler. If yes then the status is meaningless by the time the handler gets it.

If you do not think there is any useful conversation left to have about this then feel free to close the ticket.

@natefaubion
Copy link
Collaborator

The bracket mechanism is only designed for resource acquisition/release. There is potentially room for some other control mechanism, but it isn't immediately clear to me yet what that would be based on your goals.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

3 participants