Paul King created GROOVY-12326:
----------------------------------
Summary: ChannelSelect: guard an offer directly with
receive(c).when(cond)
Key: GROOVY-12326
URL: https://issues.apache.org/jira/browse/GROOVY-12326
Project: Groovy
Issue Type: Improvement
Reporter: Paul King
h2. Summary
GROOVY-12324 added a precondition mask to {{ChannelSelect}}, and it works.
Building a real guarded ALT
against it surfaced a second, separable gap: the mask binds each flag to an
offer *positionally and
anonymously*.
{code:groovy}
def alt = ChannelSelect.from(put, get) // written here
...
def r = await alt.select(counter < cap, counter > 0) // ...guarded here, by
position only
{code}
Nothing at the selection site says which channel {{counter < cap}} guards. The
reader counts arguments and
matches them against a {{from(...)}} written elsewhere -- possibly far away,
since the instance is normally
hoisted so that {{fair()}} keeps its rotation state (GROOVY-12320). That is the
same class of fragility the
mask was introduced to remove; GROOVY-12324 stopped the *index* from moving,
and left the *flag*'s
association to a branch just as implicit.
This proposes attaching the guard to the offer itself, which is what occam,
Ada, Erlang and Kotlin all do.
h2. Proposal
{code:java}
// on ChannelSelect.Offer
public Offer when(java.util.function.BooleanSupplier enabled)
{code}
{code:groovy}
def alt = ChannelSelect.offers(receive(put).when { counter < cap },
receive(get).when { counter > 0 }).fair()
while (true) {
def r = await alt.select()
...
}
{code}
h3. Why a supplier and not a plain boolean
This is the part that only shows up when you build with it. A guard must be
re-evaluated on every
selection, because the condition it tests changes every iteration -- that is
the whole point of a bounded
buffer. An {{Offer}} is documented as "Immutable and freely reusable across
selects", and a select instance
is normally built ONCE and hoisted out of the loop so that {{fair()}} can keep
its rotation state.
So {{when(boolean)}} would be wrong in two different ways:
* built once and hoisted, it freezes the guard at construction and every later
selection uses a stale flag;
* rebuilt each iteration to avoid that, it discards the {{lastWinner}} rotation
-- reintroducing exactly the
bug GROOVY-12320 was filed to fix, and which the checker already refuses as
"fair() on a fresh
ChannelSelect instance each iteration keeps no rotation state (priority in
effect)".
A {{BooleanSupplier}} evaluated at each {{select()}} call is the only form that
keeps a held instance and a
per-iteration guard at the same time. In Groovy it reads as a closure, and a
closure over a mutable local
sees the updates, so the bounded-buffer idiom above works as written.
h3. Interaction with select(boolean...)
An offer's {{when}} and a flag passed to {{select(boolean...)}} should be
*conjoined*: the offer is enabled
when both hold. That is the only reading that does not silently ignore one of
the two. (Rejecting the
combination outright is also defensible; what should not happen is one quietly
winning.)
Everything else is inherited unchanged: indices are preserved, and if every
offer ends up disabled the
returned awaitable already fails with {{IllegalStateException}}.
h2. Open question -- worth settling before the API ships
Nothing carrying GROOVY-12324 has been released yet, so the shape is still
free. That makes this the moment
to ask a question that cannot be asked afterwards: *do we want both forms, or
should {{when}} be the only
one?*
The argument for keeping both is that they serve different entry points rather
than duplicating each other:
* {{select(boolean...)}} works directly with {{from(channels...)}}, which is
what most code writes, and is
the direct port of JCSP's {{priSelect(boolean\[\])}} and GPars'
{{select(List<Boolean>)}} -- useful to
anyone migrating;
* {{when}} needs {{offers(receive(c)...)}}, which is more verbose for the
common all-receives case, but
binds guard to branch syntactically.
If both are kept, the javadoc should lead with {{when}} as the idiomatic form
and present the mask as the
positional/compatibility one. If only one survives, it should be {{when}}.
h2. Prior art
Attaching the guard to the branch is the majority design; the positional mask
is the minority one, and it
exists mainly where the result is a bare index.
|| System || Guard is written... || Bound to its branch by ||
| occam / occam-pi | {{bool & c ? x}} | syntax |
| Ada | {{when Cond =>}} on a select alternative; entry barriers | syntax |
| Erlang / Elixir | {{Pattern when Guard ->}} | syntax |
| Kotlin coroutines | conditional clause registration in {{select \{ \}}} | the
clause itself |
| Go | {{nil}} channel in the case expression | the case |
| JCSP | {{priSelect(boolean\[\] preCondition)}} | position |
| GPars | {{select(List<Boolean> mask)}} | position |
| Groovy 6 today | {{select(boolean... enabled)}} | position |
h2. Implementation sketch
Small, and contained to {{Offer}} plus one line in the registration loop.
* {{Offer}} gains a {{BooleanSupplier enabled}} field, null meaning "always".
{{when(...)}} returns a new
{{Offer}} with the same channel/value/send and the given supplier, preserving
immutability and
reusability.
* {{registrationOrder}} already skips offers the mask disables; extend its test
from
{{enabled == null || enabled\[i\]}} to also consult
{{offers.get(i).enabled}}, evaluating the supplier
once per {{select()}} call. The existing empty-order path then produces the
{{IllegalStateException("every offer of the select is disabled")}} with no
further change.
* No change to {{Result}}, the claim protocol, {{withdraw}} or {{resend}}.
* {{from(...)}} is unaffected: it builds plain receive offers with no guard.
h2. Provenance
Found by building a static verifier's model of GROOVY-12324 against Jon
Kerridge's UCaPE c05 {{Queue}}
(the bounded circular buffer that masks both of its guards) -- the same tool
and the same corpus behind
GROOVY-12320, GROOVY-12323 and GROOVY-12324. The mask itself is not at fault
and needs no change; this is
about where the guard is written.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)