Open wayland opened 2 years ago
Initial Proposal
Attached is a slightly modified version of the document I posted to the perl6-users mailing list.
Hopefully it's not too detailed. My intent was to make it just detailed enough to get my ideas across. Unfortunately this required the presentation of a few (orthogonal) ideas.
Please note that if by "implement the solution", it's meant "write documents (and/or translate them to Markdown)", then I can work on it, but if it's meant "implement in the language grammar", then I'm at the very least going to need a fair whack of help.
Hope this is useful. Thanks for your time!
@wayland I'm sorry to say this as I realize what amount of work have you done already, but it'd be much more practical to have your proposal posted here, in a comment. First of all, it would make it much easier to comment by referring to particular parts of the proposal. And this might be my personal discomfort only, but I don't like this switching between browser and PDF viewer as it draws my focus away.
Anyway, @Xliff had some thoughts on this they posted in the mailing list. Perhaps worth moving over here?
My personal first reaction: Fortran, Basic, aren't this you, my old pals? Perhaps, some would think of Cobol too. To be frank, I haven't read it all, just browsed quickly.
Overall, the idea worth considering. The weak points I see from the start are block-less phasers like BEGIN say "Compiling";
. The current form is compact and serves the purpose of quickly inserting something useful in the code. COMPILE ENTER say "Compiling";
?
Going more Raku-way is, perhaps, have adverbs involved. For block phasers this would look pretty natural:
COMPILE :in { # BEGIN
...
}
COMPIE :out { # END
...
}
:in
could be the default. This way it would also emphasize the fact that we modify the phaser behavior. Yet, COMPILE :out say "End compiling"
still looks rather line noisy, comparing to END say "End compiling";
. OTOH, one-word aliases are to be kept anyway.
New control statements? Better not. Besides, assert
neither let you specify the exception, nor it's arguments. This would limit it's usage. Yet, in a well-classified world of phasers I would consider the following for iterations:
ENTER :once { ... }
ENTER :assert(* < 3) { X::MyException.new: :why("because too low") }
ENTER :assert($item < 3) { X::MyException.new: :why("because too low") }
In the most extreme case, the :assert
variant could even be shortened to:
ENTER :assert(* < 3), X::MyException, :why("because too low");
All the .new.throw
kitchen would be hidden then.
Again, on things like LEAVE .entry-success
. Since our current syntax of $_ ~~ :foo
means .foo.Bool
, LEAVE :entry-success
would basically wind down to the same semantics.
But otherwise more in-depth analysis is apparently required as one way, or another, but the idea introduces more verbosity.
This is my conversion of the above PDF into Markdown (with a couple of bugfixes).
Edition 0.3
One well-known part of the Raku philosophy is "Make the easy things easy, and the hard things possible". I'd like to propose a small addendum "...and the orthogonal things orthogonal".
I love phasers. I love the idea of them, anyway. Once you actually start using them, reality shows up. My experience with phasers is that:
a) The ones that I want to use often don't exist anyway, because the orthogonal things haven't been kept orthogonal.
b) There are a lot of them, and I have to look them up if I want to use them. "Set phasers to the max" sounds fun ... until you have to remember all their names.
This proposal will make phasers much more flexible. But only if it actually gets implemented. On the down side, it will also make them more wordy. But on the up side again, instead of remembering 20 words, it will be a combination of about 10 words, and will present many more possibilities than the current phasers.
The things added will be:
given {}
. The things removed will be 18 of the 20 current phasers. While still having more flexibility than before.
Throughout this document, I will refer to the Current Style (ie. current Raku) and the New Style (ie. my suggestion). While I'm not proposing that we eliminate the existing phasers (they can remain as aliases), I'm not against it either. I won't hold you back if people really want to ... fire the phasers!
Given that my knowledge of Raku internals (or even asynchronous phasers) ranges from none to minimal, this proposal should be considered merely a beginning for discussion. But it may spark some ideas about orthogonality.
It will be useful to start by listing the Current Style Phasers, and suggesting a replacement for each in the New Style. Note that this does not demonstrate any of the newly-possible functionality. The default topic for the LEAVE
phaser is the &?BLOCK
variable (but see below under class Block (Enhanced)), to the extent that if the topicaliser starts with a '.
' (eg. .block-done
), then that's considered to be &?BLOCK.block-done()
. However, the topicaliser could also be another block label, or a variable containing a block, or something like that. There's nothing ... blocking you from doing that.
BEGIN {} COMPILE ENTER {}
CHECK {} COMPILE LEAVE {}
INIT {} RUNTIME ENTER {}
END {} RUNTIME LEAVE {}
DOC * DOC * (No change)
ENTER {} ENTER {} (No change)
LEAVE {} LEAVE {} (No change in syntax)
KEEP {} LEAVE { .success and do {} }
UNDO {} LEAVE { .success or do {} }
FIRST {} ENTER { once {} }
NEXT {} LEAVE { when .iter-done.not {} }
LAST {} LEAVE { when .iter-fine {} }
PRE {} ENTER { assert {} }
POST {} LEAVE { assert {} }
CONTROL {} LEAVE { when .block-done ~~ X::Control {} }
CATCH {} LEAVE { when .block-fine.not {} }
QUIT {} LEAVE { when .iter-fine.not {} }
LAST {} LEAVE { when .iter-fine(CX::Done) {} }
CLOSE {} LEAVE { when .iter-done ~~ CX::Done {} }
COMPOSE {} COMPOSE ENTER {}
A lot of the above is relatively self-evident in its meaning. The parts that are the least evident are the new methods on Block. So I feel like I'll clarify things the most by starting there.
These methods are being added to Block to support the functionality above.
method block-done(--> Bool|Exception)
How was the block exited?
Possible return values and meanings are:
Bool False
: Not complete yet, or no entry-success (see entry-success, below)Bool True
: No Exception (ie. normal exit)Exception
: How the block exited. Note that this is not throwing an exception, but returning one. method entry-success(--> Bool)
Was the the block successfully entered?
Returns True. Will be overridden by:
Iterating
: see belowRoutine
: Currently, LEAVE
blocks in a Routine currently run even if the parameter binding fails (with no currently-documented way of avoiding this). To avoid this problem, use LEAVE { when .entry-success {} }
method block-fine(Exception @exceptions --> Bool)
Did the block exit fine (if you'll pardon the English/Italian pun)?
The funtion is trivial, but convenient. Pseudo-code is:
method block-fine(Exception @exceptions) {
@exceptions or @exceptions = (X::Control);
given self.block-done {
when Bool { return $_; }
when any(@exceptions) { return True; }
default { return False; }
}
}
method success(--> Bool)
What was the success value of the block?
This takes the Defintion of Success (see https://design.raku.org/S04.html#Definition_of_Success ) and makes it no longer implementation-defined. It adds the idea that, if entry-success
is False
, then it's False
. The short (pseudo-code) version is:
.block-done ~~ all(Bool, True) ?? $return-value !! False
Apologies if this concept already exists, but I didn't see anything in the documentation. User yary on perl6-users suggested that "I think all the methods proposed for Block & Iterating should instead go on an object that exposes the control flow/program counter. Is there such a thing already?"
class Iterating is Block
The Iterating is a descendant of Block, and an ancestor to loop
, supply
, and react
blocks. Because I intend it to be an ancestor for all loops (not just ones with an iterator) as well as supply
and react
blocks, possibly it should have a different name.
method entry-success(--> Bool)
Was the the block successfully entered?
Overrides the method on the parent Block. This is True
when the Iterating has had at least one item provided to it, but is False
if the Iterating has no items provided to it.
method iter-done(--> Bool|Exception)
How was the Iterating exited?
The possible return values are the same as for Block.block-done()
, except that they apply to the completeness of the Iterating (loop/supply
/react
), and not the completeness of the block.
method iter-fine(Exception @exceptions --> Bool)
Did the Iterating exit fine?
This is the same as block-fine
, but based around .iter-done
instead of .block-done
.
I'd like to propose a new Magic Variable, &?ITERATING
, which is like &?BLOCK
and &?ROUTINE
, but for the nearest containing Iterating block.
The following changes would be useful.
This is a modified version of once
. It's like the current once
, but takes a block label (or Block) as a parameter, and happens only once within that block. It will reset (to run again) after the specified block is completed (or when the next one starts).
The default value is &?ITERATING
. To get something like the current behaviour, we probably want to pass in something like &?PACKAGE
. Unless there's a larger scope for the whole program that we could use instead.
First example: how the new structure would work without a block label:
for [1, 2, 3] -> $outeritem {
for <a b c> -> $inneritem {
print "$outeritem"
once { print "--" }
print "$inneritem, ";
}
}
say;
# 1--a, 1b, 1c, 2--a, 2b, 2c, 3--a, 3b, 3c
The exact same code, but we pass a block label
OUTER: for [1, 2, 3] -> $outeritem {
for <a b c> -> $inneritem {
print "$outeritem"
once OUTER { print "--" }
print "$inneritem, ";
}
}
say;
# 1--a, 1b, 1c, 2a, 2b, 2c, 3a, 3b, 3c
assert { <expression> }
This is shorthand for the following:
if (<expression>) {raise Exception...}
The reason for this is to simplify the Current Style PRE/POST phasers.
The following is pseudocode, but should get the idea across.
rule phaser { <prefix>* <phaser-name> <topic>? <block> }
rule phaser-name { 'ENTER' | 'LEAVE' }
rule prefix { 'DOC' | 'RUNTIME' | 'COMPILE' | 'COMPOSE' }
From this, it will be observed that remaining phasers are:
ENTER
: When the block (or prefix-defined item) is entered. LEAVE
: When the block (or prefix-defined item) is left. However, just as putting a LEAVE inside a Routine that doesn't successfully bind parameters, putting a LEAVE in an Iterating should also activate if there are no iterations of the Iterating. Regarding execution order within a queue, it’s the same as declaration order. This actually gives more flexibility (especially as far as ordering goes), but is not backwards compatible.
for 1..3 -> $item {
say "Item is $item";
ENTER { say "new loop"; }
FIRST { say "first"; }
PRE { $item < 3; }
}
for 1..3 -> $item {
say "Item is $item";
ENTER {
assert { $item < 3; }
once { say "first"; }
say "new loop";
}
}
The two code samples above are equivalent. In the Current Style, the order is fixed by the phaser ordering. In the New Style, while the order is the same as the other code sample, the option is available to change the ordering of the 3 lines in the ENTER
block. More flexibility.
The prefixes are:
DOC
: Almost exactly the same as the one already in the raku documentation. Implies COMPILE
(unless declared RUNTIME
)RUNTIME
: Makes the phaser happen at runtime, as early/late as possibleCOMPILE
: Makes the phaser happen at compile time, as early/late as possibleCOMPOSE
: Runs when a role is composed into a classObviously, RUNTIME
and COMPILE
override each other.
The New Style provides a number of advantages. Some have already been shown, but a couple more examples might be useful.
Only run the LEAVE
code if the exit was a fallthrough, rather than a Control Exception.
for [1, 2, 3, 4, 5] -> $item {
if $item == 6 {
last;
}
LEAVE .block-done {
when all(Bool, True) { say "No items match the special six"; }
}
}
That feature alone should practically justify the new system. However, there are many others.
For example:
@array = ();
for @array -> $item {
say "Item is $item";
LEAVE {
when .entry-success { say "I wannan Item! Gimmie Item! .... no Item :("; }
}
}
The code inside the when .entry-success {}
only runs if there are no items in @array
.
Things that might need changing are:
ASYNC
prefix to declare a Phaser asynchronous, that's an option too. Block.block-(done|fine)
with Block.(done|fine)
and Iterating.iter-(done|fine)
with Iterating.(done|fine)
. This might have advantages, but would probably also necessitate a restructure of the code, so I've avoided it.Because of the collapsing of most Phasers into ENTER
/LEAVE
, Phaser queues have become somewhat irrelevant.
Hopefully this will provide a starting point for a discussion about phasers, and how they might be made easier to work with.
• Tim Nelson (original document)
• Clifton Wood (syntax improvements)
• yary (suggestion about Iterating)
@vrurg
&?BLOCK
and &?ITERATING
part works (or whatever replaces them). IN:compile
would be the better alternative; I see ENTER
/LEAVE
(or IN
/OUT
) as the two basic phasers, and everything else is just a modification of them. once
, this is something I'm keen to use regularly (and not just in phasers). It's quite often that I want to do something only the first time through the loop (and I believe that this is the itch that FIRST
was designed to scratch), but find that I don't always want to do it at the start of the loop. If the extra wordiness bothers you, I can propose that we eliminate FIRST
since it will no longer be necessary :) . PRE
/POST
work with the new phasers scheme where there are only two phasers, ENTER
/LEAVE
(or IN
/OUT
, or whatever). The whole point of PRE
/POST
(if I understand) is that, if the phaser returns false, an exception is thrown. I'm assuming assert {}
would throw the same exception. I'm not particularly attached to it since I don't really use PRE
/POST
. This is just keeping the orthogonal things orthogonal. The problem with your syntax is that it's a lot wordier than PRE {}
, and a fair bit wordier than ENTER { assert{} }
, which was my suggested alternative. LEAVE .entry-success
Thanks for your suggestions.
Here my thoughts on some parts of the proposal.
I'd like to propose a new Magic Variable,
&?ITERATING
No need for it. If necessary, a block could be referenced via a label. This will require some support from the compiler, but then one can do:
FOO: for ^10 {
say FOO.block;
once FOO say "once per loop";
}
I consider this the only correct approach if functionality of once
is to be extended. Consider the following:
FOO: for ^10 {
when 5..7 {
once "once per `when`";
once FOO "once per loop";
}
}
Though since such a cardinal change of once
semantics wouldn't be appreciated, within the adverb approach it would be more correct to use ENTER
:
FOO: for ^10 {
when 5..7 {
ENTER :once say "once per `when`";
ENTER :once(FOO) say "once per loop";
ENTER :once(UNIT) say "once per module load";
ENTER :once(GLOBAL) say "once per run";
}
}
ENTER :once
bound to the innermost block is a great way to lazy initialize something only when it is really needed.
assert
Unfortunately, it wouldn't be able to replace PRE
/POST
. The latter do not belong to loop's block:
CATCH { default { say "caught in the outer: $_" } }
for ^10 {
CATCH { default { say "caught in loop: $_" } }
PRE { die "PRE died"; }
}
But ENTER
does. So, PRE
/POST
will be there. Any replacement would only add to verbosity with no apparent advantage.
@array = ();
for @array -> $item {
say "Item is $item";
LEAVE {
when .entry-success { say "I wannan Item! Gimmie Item! .... no Item :("; }
}
}
This won't work. The block will never enter in first place. So, there will be no LEAVE
. I barely see really common use-case scenario for this and why if !@array { cry-in-sorry }
is so bad to be used in first place. But if there is one then it really looks like a job for PRE
/POST
.
- I see the BASIC resemblance; I only did FORTRAN for half an hour, so no comment. Personally, I'd think more of Pascal.
Whatever. Extra verbosity is their common property. :)
- Yes, you're right, block-less phasers would probably be a thing of the past
No, they won't. Taking into account how popular and useful the following is:
for ^10 {
say now - INIT now;
sleep 1;
}
NB FIRST
would be more intuitive, but, apparently, it doesn't return a value. I don't remember wether it is intentional behavior and it looks more like a bug or an overlook.
Yet, if a sensible topic would be considered needed then it doesn't mean a block is required. Consider "foo" ~~ .say
. In this case the only question is what a phaser would have to topicalize around. In my view it needs to be some kind of phaser handle object, not a block.
- If we're going the adverbs route, though, I think
IN:compile
would be the better alternative; I seeENTER
/LEAVE
(orIN
/OUT
) as the two basic phasers, and everything else is just a modification of them.
Don't forget about readability. It much easier to quickly sour out phasers when you their stage at the first glance.
- I'm sorry, I didn't follow your last point about
LEAVE .entry-success
Consider a phaser declaration to be a kind of when
statement. For example, with when
:
class Foo {
method bar1 { False }
method bar2 { True }
};
given Foo.new {
when :bar1 { say "bar1" }
when :bar2 { say "bar2" }
default { say "Nah...." }
}
In this context LEAVE .entry-success
can be re-stated in in pseudo-code as when $phaser.leave & $phaser.entry-success
. Or, in full, this would be equivalent to:
given $phaser {
when :leave & :entry-success { ... }
}
Within the rules of your proposal this can be stated as LEAVE :entry-success
.
Though in practice, I currently see the system of the extra methods on phaser topic (no matter if it is a block or a special object) somewhat over-engineered. Not only it is complex and hard to follow. But it would require quite a bit of supporting code to be added to blocks. It is a known fact that LEAVE
alone is currently rather costly on performance. Additional price tag on any phaser would be inevitable with all that tracking of block outcomes.
Oh, and BTW:
CONTROL {} LEAVE { when .block-done ~~ X::Control {} } CATCH {} LEAVE { when .block-fine.not {} }
No, and no! First of all, it is too verbose. Second, it harms readability a lot. And instead of using when
to sort out exceptions one would need to have an additional given
to topicalize .block-done
. So, these two are most certainly untouchable.
My last thought on the whole approach. While the idea of providing some topic to phaser code may prove be useful, relying on when
inside the code is far from the beast approach from the optimization/performance point of view. The best is when we can give the compiler enough hints to do as much work at compile time as possible. So, that's what is good about my ENTER :once(FOO)
example: all is known compile-time. Same applies to CATCH
/CONTROL
. Contrary, ENTER { when ... }
and LEAVE { when ... }
offload all work onto runtime shoulders, with all the slowdowns.
Then again, it doesn't mean that the run-time approach doesn't have its use. But what is possible must be carefully considered as some scenarios would require a lot of extra tracking of block enter/leave.
&?ITERATING
: Regarding there being no need for it, I've had a good think, and I agree. once
: it's a good point about such a change not being appreciated. I'd be happy if it were called once-per
or something like that (with once
being then an alias for once+parameter). My objection to the adverb approach is that it's then tied to the ordering of the phaser (or am I making a mistake here?), rather than giving freedom of ordering, as I was hoping to achieve here (as another piece of flexibility). You're probably aware, but just clarifying, PRE was designed to be used as follows (current working Raku code):
CATCH { default { say "caught in the outer: $_" } }
for ^10 {
CATCH { default { say "caught in loop: $_" } }
PRE { 1 == 3 }
}
# caught in the outer: Precondition '{ 1 == 3 }' failed
I'm suggesting this be replaced by (proposed Raku code):
CATCH { default { say "caught in the outer: $_" } }
for ^10 {
CATCH { default { say "caught in loop: $_" } }
ENTER { assert { 1 == 3 } }
}
# caught in the outer: asserted condition '{ 1 == 3 }' failed
My point is kind of hat you don't need die
in a PRE block -- it does it itself.
While you're right that it adds verbosity, I think it has two advantages:
OK, that's 4 advantages. Mea cupla.
The original syntax I had (I changed it after @Xliff's comments) was:
CONTROL {} LEAVE .block-done { when X::Control {} }
CATCH {} LEAVE .block-fine { when False {} }
My theory was that the second one would smartmatch .block-fine against False, and match, but that may not be correct. According to the following quote from my document, that would eliminate the need for a given {}
:
The default topic for the
LEAVE
phaser is the&?BLOCK
variable, to the extent that if the topicaliser starts with a '.
' (eg..block-done
), then that's considered to be&?BLOCK.block-done()
.
If we return to my original suggestion for the replacements for CONTROL
/CATCH
, that'd eliminate at least the complaint about the extra given {}
. It may not be quite as readable, but given that CATCH
is the only phaser I use on the regular (because it's one of the few whose use I remember), let me just re-iterate that I'm suggesting that we keep the existing phasers as aliases of the new ones.
Hmm. Maybe the solution (just thinking out loud here) is to add new IN
/OUT
phasers as the only two phasers that take topics and require blocks. Then the existing syntax could remain, but if it's useful (as decided by the language implementors), could be rewritten into the new phasers. Then, if people want to stick to the existing phasers, they can, and if they want to just use IN/OUT for everything, that'd work too.
Thanks for taking the time to engage with what I'm suggesting here.
HTH,
I don't have time to look closely at this and critique it, but will note that:
BEGIN
runs at "begin time", for example, which we use to refer to many other things that happen within the parse phase, ditto for CHECK
and "check time".@jnthn
Thanks for your input!
@vrurg
On:
CONTROL {} LEAVE { when .block-done ~~ X::Control {} }
CATCH {} LEAVE { when .block-fine.not {} }
You wrote:
No, and no! First of all, it is too verbose. Second, it harms readability a lot.
And instead of using when to sort out exceptions one would need to have
an additional given to topicalize .block-done. So, these two are most certainly
untouchable.
From a purely syntactical standpoint, the when .block-done ~~ CX::Control
is fine, since the topic is to be set for &?BLOCK, which... in1 most cases is &?ROUTINE. No additional given is necessary.
Given that CONTROL and CATCH would not be going away, this should not affect readability. However in situations when .block-done is not an X::Control, this might give someone try()-adverse a different option.
You're probably aware, but just clarifying, PRE was designed to be used as follows (current working Raku code):
You seemingly missed the point I was actually making: the scoping. PRE
/POST
semantics is outer to loop's block despite of syntax being about inner declaration. So, when PRE
throws for whatever reason it the outer CATCH
which gains control.
Contrary, the proposed ENTER
is inner both syntactically and semantically. assert
, as ENTER
subsidiary, is also lexically scoped in loop's block. Hence it can't be PRE
replacement.
Speaking of CATCH
/CONTROL
: @wayland, @Xliff what would be your proposal to replace the following:
CATCH {
when X::Method::NotFound { ... }
when MyException { ... }
when X::AdHoc { ... }
default { ... }
}
Hmm. Maybe the solution (just thinking out loud here) is to add new
IN
/OUT
phasers as the only two phasers that take topics and require blocks.
And here we're starting to look for ways to get around our own mistakes. Lately when I see myself in a position like this, the next thing to do is usually to step back and re-consider the whole concept. Good when it is a mental model, and there is nothing to be re-done yet...
Anyway, I'd like to remind you, that topicalization is not necessarily bound to a block. If you read carefully about the smartmatch op, you'll find out that it topicalizes over its LHS, so RHS can use it. The topic exists only for the time to complete the operation and then reverts back to the lexical topic:
given "foo" {
"bar" ~~ .say;
.say;
}
Hope it'd help you to re-consider some points.
- Phasers in some cases chime with a more broadly used concept.
BEGIN
runs at "begin time", for example, which we use to refer to many other things that happen within the parse phase, ditto forCHECK
and "check time".
@jnthn Basically, I don't see why BEGIN
cannot be an alias for COMPILE :in
, and CHECK
- for COMPILE :out
.
It's more about other shortcomings of the proposal which are currently making it unreasonable to be considered as a realistic candidate. And while the idea of orthogonalization of the phasers is appealing, but the number of different modifiers and methods, required to cover all possible cases, leads back to about the same complexity, the author tries to avoid.
@vrurg :
Huh, interesting point about the PRE/POST and scope (thanks for clarifying!). On my local Rakudo, I get:
CATCH { default { say "caught in the outer: $_" } }
for ^10 {
CATCH { default { say "caught in loop: $_" } }
ENTER { die "ENTER died"; }
}
# caught in the outer: ENTER died
So that means that either I've misunderstood you again, or that Rakudo doesn't follow the spec. Most likely I've misunderstood you.
My initial reaction is something like:
LEAVE .block-done {
when X::Method::NotFound { ... }
when MyException { ... }
when X::AdHoc { ... }
if(! &?OUTER::BLOCK.block-fine) { ... }
}
That does highlight another weakness in my plan; if the topic isn't the parent block, then the parent block is hard to get at (this is a problem with the current CATCH too, but since these block-methods aren't used in existing phasers, it's not as much of a problem). There should definitely be a good solution to this. Options I can see include:
LEAVE .block-fine {
when X::Method::NotFound { ... }
when MyException { ... }
when X::AdHoc { ... }
when Exception { ... }
}
That's probably the solution. That's probably acceptable. .block-fine by itself will filter out the X::Control exceptions. But if there's another solution you like better, I'd be interested in hearing it.
Regarding this leading back to the same complexity, while that may well be true (there are fewer syntactic elements, but of a wider variety of kinds), I think the extra flexibility is worth it.
If you (currently) deem it "unreasonable to be considered as a realistic candidate", I have a question: are there any parts of this proposal that you do like? I ask because we might be able to take these parts and turn them into something else.
HTH,
So that means that either I've misunderstood you again, or that Rakudo doesn't follow the spec. Most likely I've misunderstood you.
Likely not, but it rather seems like a bug in implementation. This semantics is neither specced nor documented. But following quote from the original synopses makes me think that my interpretation of ENTER
as lexical of the surrounding block is correct:
An exception thrown from an
ENTER
phaser will abort theENTER
queue, but one thrown from aLEAVE
phaser will not. The exceptions thrown by failingPRE
andPOST
phasers cannot be caught by aCATCH
in the same block, which implies thatPOST
phaser are not run if aPRE
phaser fails.
There would be no point of focusing on the outer CATCH
for PRE
unless exceptions from ENTER
/LEAVE
are to be handled by the inner CATCH
.
If you (currently) deem it "unreasonable to be considered as a realistic candidate", I have a question: are there any parts of this proposal that you do like?
I like the general idea of orthogonalization. But considering all the special cases, each of which would require extra modifiers or alike, I'm not sure there is any concise solution.
PRE/POST: Interesting; I interpret the part about the PRE
/POST
focus as a preamble to the rest of the sentence, that "POST
phasers are not run if a PRE
phaser fails". But looking carefully at the Exection Order, especially the words immediately before the PRE
and ENTER
lines, you may be correct.
(and incidentally, @JJ , the original doco and vrurg both wrote "POST
phaser", rather than "POST
phasers"; alternately, it should be "POST
phaser is...").
I still feel like Phasers are at a lower level of orthogonality, but it may require someone smarter than me to come up with the solution. Or at least someone who gets one more interesting idea to throw into the mix. Or maybe in Rakudo version 20 when we have 128 phasers, the solution will become obvious :-p .
OK, I did some thinking (thanks @vrurg ), and realised that the one time people don't mind wordiness is when they're writing code (ie. they don't care how long the code is as long as it provides a new class/routine/whatever). So with that thought, I rewrote the document to instead just allow user-defined phasers. Welcome to version 1.0 (up from 0.3).
Edition 1.0
One well-known part of the Raku philosophy is "Make the easy things easy, and the hard things possible". I'd like to propose a small addendum "...and the orthogonal things orthogonal".
I love phasers. I love the idea of them, anyway. Once you actually start using them, reality shows up. My experience with phasers is that the ones that I want to use often don't exist anyway, because the orthogonal things haven't been kept orthogonal.
This proposal will make phasers much more flexible. But only if it actually gets implemented. The things added will be:
BLOCK
(new methods, etc)once-per
throw
Note that I'm not proposing that we eliminate the existing phasers. It may be advantageous to rewrite some(but probably not all) in the new syntax.
Given that my knowledge of Raku internals (or even asynchronous phasers) ranges from none to minimal, this proposal should be considered merely a beginning for discussion. But it may spark some ideas about orthogonality.
It should be possible for a user to define their own phasers.
Unlike the previous proposal, this does not propose any new phasers, merely a phaser definition syntax.
None of the existing phasers will be ... phased out. But it might be illustrative to define existing phasers using the newly-proposed syntax.
phaser CATCH(Block &passed-block) trigger BLOCK Exception {
when .fine.not {
&passed-block(.done)
}
}
phaser PRE(Block &passed-block) trigger BLOCK Entry {
if ! &passed-block() {
X::Phaser::PrePost.new(phaser => "PRE", condition => "\{ 1 == 3 }").throw(OUTER);
}
}
I'm not proposing that we actually do such a rewrite, merely noting that it's possible.
In the examples above, you will have noticed the new keyword 'trigger', followed by a Phasable and a queue type.
The following queue types may be deemed useful:
Note that each of these types intersects with all the Phasables below, so that the actual queues have names like "The BLOCK Entry queue".
The Phasable is one of the following:
UNIT
: The moduleGLOBAL
: The entire execution (ie. runtime)COMPILING
: The compile phaseOUTER
: The next outer scopeDOC-COMPILE
: Like COMPILE, but in doco modeDOC-GLOBAL
: Like GLOBAL, but in doco modeBLOCK
: The blockLOOP
: The loop that wraps around a block. This also includes supply and react blocks. ROUTINE
: The current routineCOMPOSE
: A role being composed into a classThere may be other Phasables. These Phasables are also the topic for the phaser definition block. This is why CATCH
, in the above example, can use .fine, for example (which is BLOCK.fine
).
It may be useful to break down the Phasables and their methods.
To support the necessary functionality, the following methods will be useful on the BLOCK
Phasable.
method done(--> Bool|Exception)
How was the block exited?
Possible return values and meanings are:
Bool False
: Not complete yet, or no entry-success (see entry-success, below)Bool True
: No Exception (ie. normal exit)Exception
: How the block exited. Note that this is not throwing an exception, but returning one. method entry-success(--> Bool)
Was the the block successfully entered? This is necessary because LEAVE routines can run even if a block was never entered.
Returns True for BLOCK.
method fine(Exception @exceptions --> Bool)
Did the block exit fine (if you'll pardon the English/Italian pun)?
The funtion is trivial, but convenient. Pseudo-code is:
method fine(Exception @exceptions) {
@exceptions or @exceptions = (X::Control);
given self.done {
when Bool { return $_; }
when any(@exceptions) { return True; }
default { return False; }
}
}
method success(--> Bool)
What was the success value of the block?
This takes the Defintion of Success (see https://design.raku.org/S04.html#Definition_of_Success ) and makes it no longer implementation-defined. It adds the idea that, if entry-success
is False
, then it's False
. The short (pseudo-code) version is:
.done ~~ all(Bool, True) ?? $return-value !! False
method done(--> Bool|Exception)
How was the LOOP exited?
The possible return values are the same as for BLOCK.done()
, except that they apply to the completeness of the LOOP, and not the completeness of the block.
method entry-success(--> Bool)
Was the the block successfully entered?
This is True
when the LOOP has had at least one item provided to it, but is False
if the LOOP has no items provided to it.
method fine(Exception @exceptions --> Bool)
Did the LOOP exit fine?
This is the same as BLOCK.fine
(but on LOOP).
Same as BLOCK, but with the following exception:
method entry-success(--> Bool)
Currently, LEAVE
blocks in a Routine currently run even if the parameter binding fails (with no currently-documented way of avoiding this). To avoid this problem, use LEAVE { when .entry-success {} }
This is a modified version of once
. It's like once
, but takes a block label or Phasable as a parameter, and happens only once within that block. It will reset (to run again) after the specified block is completed (or when the next one starts).
The default value is &?LOOP
.
First example: how the new structure would work without a block label:
for [1, 2, 3] -> $outeritem {
for <a b c> -> $inneritem {
print "$outeritem"
once-per { print "--" }
print "$inneritem, ";
}
}
say;
# 1--a, 1b, 1c, 2--a, 2b, 2c, 3--a, 3b, 3c
The exact same code, but we pass a block label
OUTER: for [1, 2, 3] -> $outeritem {
for <a b c> -> $inneritem {
print "$outeritem"
once-per OUTER { print "--" }
print "$inneritem, ";
}
}
say;
# 1--a, 1b, 1c, 2a, 2b, 2c, 3a, 3b, 3c
They should take an extra parameter that's a phasable, and indicate that we should drop out to that scope before throwing the exception. I can't see any good uses for this except OUTER (for the PRE and POST phasers).
The Phaser Definition Syntax provides a number of advantages. Some have already been shown, but a couple more examples might be useful.
Only run the LEAVE
code if the exit was a fallthrough, rather than a Control Exception.
phaser NATURAL_LEAVE(Block &passed-block) trigger BLOCK Exit {
when .done ~~ all(Bool, True) {
&passed-block()
}
}
for [1, 2, 3, 4, 5] -> $item {
if $item == 6 {
last;
}
NATURAL_LEAVE {
say "No items match the special six";
}
}
That feature alone should practically justify the new system. However, there are many others.
For example:
phaser INSTEAD(Block &passed-block) trigger LOOP Exit {
.entry-success or do { &passed-block() }
}
@array = ();
for @array -> $item {
say "Item is $item";
INSTEAD {
say "I wannan Item! Gimmie Item! .... no Item :(";
}
}
The code inside the INSTEAD
only runs if there are no items in @array
.
Things that might need changing are:
is ASYNC
modifier to declare a Phaser asynchronous, that's an option too. Hopefully this will provide a starting point for a discussion about phasers, and how they might be made easier to work with.
I'm not going to do the COMPILING/GLOBAL/COMPOSE ones (such as BEGIN/END/INIT/CHECK/COMPOSE) because they're probably best left alone. And again note that I'm not saying the following should be rewritten this way, just that it's illustrative to see how they could be rewritten.
phaser ENTER(Block &passed-block) trigger BLOCK Entry {
&passed-block();
}
phaser LEAVE(Block &passed-block) trigger BLOCK Leave {
&passed-block();
}
phaser KEEP(Block &passed-block) trigger BLOCK Leave {
when .success { &passed-block(); }
}
phaser UNDO(Block &passed-block) trigger BLOCK Leave {
when not(.success) { &passed-block(); }
}
phaser FIRST(Block &passed-block) trigger LOOP Entry {
once-per $_ { &passed-block(); }
}
phaser NEXT(Block &passed-block) trigger LOOP Exit {
when not(.done) { &passed-block(); }
}
# Synchronous LAST
phaser LAST(Block &passed-block) trigger LOOP Exit {
when .fine { &passed-block(); }
}
phaser PRE(Block &passed-block) trigger BLOCK Entry {
if ! &passed-block() {
X::Phaser::PrePost.new(phaser => "PRE", condition => &passed-block.raku).throw(OUTER);
}
}
phaser POST(Block &passed-block) trigger BLOCK Exit {
if ! &passed-block() {
X::Phaser::PrePost.new(phaser => "POST", condition => &passed-block.raku).throw(OUTER);
}
}
phaser CONTROL(Block &passed-block) trigger BLOCK Exit {
when .done ~~ X::Control {
&passed-block(.done)
}
}
phaser CATCH(Block &passed-block) trigger BLOCK Exit {
when .fine.not {
&passed-block(.done)
}
}
# Not sure about this one
phaser QUIT(Block &passed-block) trigger BLOCK Exit {
if not(.fine) and .done ~~ X::Proc::Async {
&passed-block(.done)
}
}
# Asynchronous LAST
phaser LAST(Block &passed-block) trigger BLOCK Exit {
if .fine(CX::Done) {
&passed-block(.done)
}
}
phaser CLOSE(Block &passed-block) trigger BLOCK Exit {
if .done ~~ CX::Done {
&passed-block(.done)
}
}
While I was reading your example I was thinking that maybe a phaser creator should work kind like a macro, I mean using AST (maybe RakuAST). I thought something like this might be cool (please ignore the faço I don't know the RakuAST classes names and I've invented some methods on it and probably some other things as well).
phaser PRE(AST::Block $in, AST::Statement $condition) {
$in.add-before: quasi( unless {{{ $condition }}} { die "PRE error" } )
}
phaser CATCH(AST::Block $in, AST::Block $block) {
$in.on-exception: $block
}
Does that make any sense?
@FCO I think that is indeed how some phasers (like ENTER, FIRST, PRE) will be implemented in RakuAST. Others will still need some mechanism in the runtime to get fired, though.
But if the suggestion is to have a custom phaser creator, wouldn't make sense to it be something like that (just adding an option to add something to runtime)? That way we could, for example create a phaser that is only allowed to be used inside a for block and not a while block, for example, allowing someone to create a FOR-ELSE parser for example.
My experience with phasers is that: a) The ones that I want to use often don't exist anyway, because the orthogonal things haven't been kept orthogonal.
b) There are a lot of them, and I have to look them up if I want to use them. "Set phasers to the max" sounds fun ... until you have to remember all their names.
Examples of things that should be achievable with phasers include:
for {} else {}
structure that he had to add a slang to get (runs the "else" part if there are no items to iterate)for {}
problem discussed at https://stackoverflow.com/questions/70281827/raku-last-on-non-loops in which a good solution is pointed out, but the docs specifically say that solution shouldn't work (though rakudo permits it, as would the not-yet-implementedleave
keyword).There are others, though.
In a lot of ways this is a request for a) the orthogonal things to be kept orthogonal, and b) more flexibility. I'm afraid this problem statement is somewhat nebulous and non-compelling. However, when I posted a suggested solution (that I'm not particularly attached to, apart from the idea of "Make the easy things easy, the hard things possible, and the orthogonal things orthogonal") on perl6-users, people seemed keen that I bring it here.