Skip to content

Initial implementation #1

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

Merged
merged 11 commits into from
Jul 18, 2017
Merged

Conversation

natefaubion
Copy link
Contributor

Implements a low-level Eff based interface for AVars with an improved API which is consistent with Haskell's MVars.

if (avar.error !== null) {
value = left(avar.error);
// Error callback ordering is somewhat undefined, but we try to at least
// be fair by interleaving puts and takes.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the logic of take/put is correct, then this is overgeneralized and supports more possibilities than can actually exist.

data AVarState a 
  = Empty 
  | Full a 
  | Surplus (NonEmptyList (Tuple a (Callback Unit))) 
  | Deficit (NonEmptyList (Callback a)) (List (Callback a)) -- takers, readers

Taking advantage of these constraints could simplify things and remove the need to handle "impossible cases", as well as provide stronger guarantees.

Note that NonEmptyList above should actually be something like NonEmptyFifoQueue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, the error handling here is overly general. It should be fine to just drain all of them because it isn't possible to have multiple producers and consumers at the same time.


// Callbacks could have queued up more items so we need to guard on the
// actual mutable properties.
if (avar.value === EMPTY && ps.size === 0 || avar.value !== EMPTY && ts.size === 0) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems you could also model changes to state as AVarState a -> AVarState a, such that only drainVar would actually compute state, the rest would just compose new state updaters together. This handles the reentrant problem.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will have a think about this, and perhaps write up a simple benchmark.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One concern I have about this is that I'm fairly certain this will complicate cancellation quite a bit. What I do now is a bit indirect, but everything is queued immediately in the appropriate place. Cancellation is then just a matter of revoking the slot in the queue. If I applied your suggestion, I would essentially be queuing the changes to the queues. I would still need the separate queues to handle the correct ordering of reads and takes, but I can't convince myself I couldn't get into a situation where it's possible to try to revoke a pending item when it hasn't been put in the appropriate queue yet. I would have to track this somehow.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've also tried to keep allocations to a minimum. With this approach, takes/reads are only a single allocation per operation (aside from the usual PS overhead), and puts are two.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction, for takes/reads there is an allocation for the queue slot, and an allocation for the canceler thunk. For puts there is the cb/value tuple, the queue slot, and the canceler.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but I can't convince myself I couldn't get into a situation where it's possible to try to revoke a pending item when it hasn't been put in the appropriate queue yet

Thinking about this again, this is not a reasonable concern. If the queue is flushed, the clearly it will be in the appropriate place before yielding the canceler. Mutation hurts my brain.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I rescind that 😆. If you start a loop, and you queue something while in the loop, the canceler will return before the item is queued properly, so you'd have to track two places in the queues. In light of this, I think I'd rather just deal with it the way I've already done it since it requires no bookkeeping, at the expensive of the guards I've inserted.

-- | Reads the AVar value. Unlike `takeVar`, this will not leave the AVar empty.
-- | If the AVar is empty, this will queue until it is filled. Multiple reads
-- | will resolve at the same time, as soon as possible.
readVar ∷ ∀ eff a. AVar a → AVarCallback eff a → AVarEff eff (AVarEff eff Unit)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like these guarantees. 👍

this.takes = new MutableQueue();
this.reads = new MutableQueue();
this.puts = new MutableQueue();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to make this accept/set value in the constructor?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because you don't always want a value. There's no real difference. In the current case, if I want a value, I just mutate it. In the other case I'd have to provide EMPTY.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's all I was suggesting, that the other case became new AVar(EMPTY), but I don't mind really.

return just(value);
}
};
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not need to check error in this case? I see it's handled in drainVar too, but just wanted to double check, as it's checked in the non-try takeVar.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we don't. When an AVar is killed, it's value is set to EMPTY.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, got it.

foreign import _tryPutVar ∷ ∀ eff a. Fn.Fn4 (Error → Either Error a) (a → Either Error a) (AVar a) a (AVarEff eff Boolean)
foreign import _takeVar ∷ ∀ eff a. Fn.Fn4 (Error → Either Error a) (a → Either Error a) (AVar a) (AVarCallback eff a) (AVarEff eff (AVarEff eff Unit))
foreign import _tryTakeVar ∷ ∀ eff a. Fn.Fn5 (Error → Either Error a) (a → Either Error a) (Maybe a) (a → Maybe a) (AVar a) (AVarEff eff (Maybe a))
foreign import _readVar ∷ ∀ eff a. Fn.Fn4 (Error → Either Error a) (a → Either Error a) (AVar a) (AVarCallback eff a) (AVarEff eff (AVarEff eff Unit))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We typically write Left / Right accepting arguments for the FFI as forall a b. a -> Either a b, etc. elsewhere as it means the implementation is obvious/only possible one way via parametricity.

@natefaubion
Copy link
Contributor Author

  • Changed the signature to new AVar(initialValue)
  • Removed special error handling. Now everything just runs through drainVar.

@natefaubion natefaubion changed the title WIP: Initial implementation Initial implementation Jul 18, 2017
@natefaubion natefaubion merged commit e734e17 into purescript-contrib:master Jul 18, 2017
@garyb
Copy link
Member

garyb commented Jul 18, 2017

🎉

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

Successfully merging this pull request may close these issues.

3 participants