More flexible Phasers
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:
- Damian Conway's
for {} else {}structure that he had to add a slang to get (runs the "else" part if there are no items to iterate) - The
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-implementedleavekeyword).
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.
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).
Phasers Mark II
Edition 0.3
Philosophy
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".
Overview
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:
- An optional topicaliser (to Phasers); this means that every phaser has a built-in, implicit
given {}. - Some additional methods on Block
- More (optional) prefixes (to Phasers)
- A modification of 'once', and a control structure 'assert'.
- Another magic variable
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.
Demonstration
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.
class Block (Enhanced)
These methods are being added to Block to support the functionality above.
method block-done
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
method entry-success(--> Bool)
Was the the block successfully entered?
Returns True. Will be overridden by:
Iterating: see belowRoutine: Currently,LEAVEblocks in a Routine currently run even if the parameter binding fails (with no currently-documented way of avoiding this). To avoid this problem, useLEAVE { when .entry-success {} }
method block-fine
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
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
class Iterating
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
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
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
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.
Magic Variable
I'd like to propose a new Magic Variable, &?ITERATING, which is like &?BLOCK and &?ROUTINE, but for the nearest containing Iterating block.
Control Flow Structures
The following changes would be useful.
once
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
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.
New Phaser Syntax
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.
Current Style
for 1..3 -> $item {
say "Item is $item";
ENTER { say "new loop"; }
FIRST { say "first"; }
PRE { $item < 3; }
}
New Style
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.
Prefixes
The prefixes are:
DOC: Almost exactly the same as the one already in the raku documentation. ImpliesCOMPILE(unless declaredRUNTIME)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 class
Obviously, RUNTIME and COMPILE override each other.
New Features Available
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.
Alternative Ideas
Things that might need changing are:
- If we also need an
ASYNCprefix to declare a Phaser asynchronous, that's an option too. - If, through discussion, we establish that an Iterating should automatically nest another block inside it (and returns from the inner block are from the current iteration, whereas returns from the outer are returns from the whole thing), then we could replace
Block.block-(done|fine)withBlock.(done|fine)andIterating.iter-(done|fine)withIterating.(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.
Conclusion
Hopefully this will provide a starting point for a discussion about phasers, and how they might be made easier to work with.
Authors
• Tim Nelson (original document)
• Clifton Wood (syntax improvements)
• yary (suggestion about Iterating)
@vrurg
- My manual conversion from PDF posted above, as requested -- hopefully this will make it easier for you to read the whole thing.
- The work of Xliff has been incorporated into my proposal
- I see the BASIC resemblance; I only did FORTRAN for half an hour, so no comment. Personally, I'd think more of Pascal.
- Yes, you're right, block-less phasers would probably be a thing of the past (unless there's another good way of distinguishing whether the following element is a topicaliser). On the other hand, perhaps we can do away with the topicaliser if we can really nail down how the whole
&?BLOCKand&?ITERATINGpart works (or whatever replaces them). - I can see the advantage of adverbs, but I was trying to change as little as possible while achieving what I wanted. If we're going the adverbs route, though, I think
IN:compilewould be the better alternative; I seeENTER/LEAVE(orIN/OUT) as the two basic phasers, and everything else is just a modification of them. - Regarding
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 thatFIRSTwas 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 eliminateFIRSTsince it will no longer be necessary :) . - Regarding assert, all I'm trying to do is make
PRE/POSTwork with the new phasers scheme where there are only two phasers,ENTER/LEAVE(orIN/OUT, or whatever). The whole point ofPRE/POST(if I understand) is that, if the phaser returns false, an exception is thrown. I'm assumingassert {}would throw the same exception. I'm not particularly attached to it since I don't really usePRE/POST. This is just keeping the orthogonal things orthogonal. The problem with your syntax is that it's a lot wordier thanPRE {}, and a fair bit wordier thanENTER { assert{} }, which was my suggested alternative. - I'm sorry, I didn't follow your last point about
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:compilewould 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 calledonce-peror something like that (withoncebeing 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).- Regarding block-less phasers still being possible, I'm glad to hear it and, while I don't quite see how it would work at the moment, I think that's something that can be sorted out later.
- Regarding the performance ramifications of adding the methods to the block-or-other object, I don't feel qualified to comment, other than to say I'm happy to hear alternatives
- Regarding LEAVE .entry-success, I think I follow
assert {}
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:
- It keeps orthogonal things orthogonal
- It means we only have to remember 2 phasers, not 20 (or at least, we can forget PRE/POST)
- assert {} can be used in other places
- It gives you flexibility with ordering; this can be seen in my example above in the New Phasers section, where I compare Current Style with New Style. The ordering in the two examples is the same, but in the New Style, it's possible to change the ordering, whereas in the Current Style it's not.
OK, that's 4 advantages. Mea cupla.
CATCH/CONTROL
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
LEAVEphaser is the&?BLOCKvariable, 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:
- Phasers in some cases chime with a more broadly used concept.
BEGINruns at "begin time", for example, which we use to refer to many other things that happen within the parse phase, ditto forCHECKand "check time". - Any proposal that wants to eliminate widely used existing syntax is not likely to go anywhere.
@jnthn
- That probably should have been obvious (but it wasn't to me!) Thanks very much for this info.
- I suspected as much. I'm expecting that we retain most, if not all, of existing syntax, at least in the short-to-medium term (and in the long term I have no idea/expectations :) ).
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/OUTphasers 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.
BEGINruns at "begin time", for example, which we use to refer to many other things that happen within the parse phase, ditto forCHECKand "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 :
PRE/POST
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.
Catch Example
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:
- Have a magic variable for the phaser-parent defined inside phasers (solves the larger topic-block problem)
- Add another method called .normal that's basically like .block-fine, but returns the exception instead of False (solves just the current problem).
- Change .block-fine to return the exception instead of False (solves just the current problem). In that case, it'd be:
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
ENTERphaser will abort theENTERqueue, but one thrown from aLEAVEphaser will not. The exceptions thrown by failingPREandPOSTphasers cannot be caught by aCATCHin the same block, which implies thatPOSTphaser are not run if aPREphaser 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/POSTfocus as a preamble to the rest of the sentence, that "POSTphasers are not run if aPREphaser fails". But looking carefully at the Exection Order, especially the words immediately before thePREandENTERlines, 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).
Phasers Mark II
Edition 1.0
Philosophy
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".
Overview
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:
- User-defined phasers
- Info about various Phasables such as
BLOCK(new methods, etc) - A new control structure:
once-per - An additional (optional) parameter to
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.
Proposal
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.
Demonstration
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.
Queue types
The following queue types may be deemed useful:
- Entry -- This happens right after the entry to the setting, and before anything else.
- Exit -- this happens just before the exit from the block.
Note that each of these types intersects with all the Phasables below, so that the actual queues have names like "The BLOCK Entry queue".
Phasables
The Phasable is one of the following:
- Many of the Pseudo-packages mentioned at https://docs.raku.org/language/packages#index-entry-OUTER_(package) including:
UNIT: The moduleGLOBAL: The entire execution (ie. runtime)COMPILING: The compile phaseOUTER: The next outer scope- Comments:
- Some of the others may also be useful, but that's probably a detail best left for later
- Yes, I know this wasn't the intended use of the Pseudo-packages; I'm suggesting expanding that in some cases
- Derived from these, we have:
DOC-COMPILE: Like COMPILE, but in doco modeDOC-GLOBAL: Like GLOBAL, but in doco mode
- The following additional Phasables can also be used:
BLOCK: 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 class
There 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.
The BLOCK Phasable
To support the necessary functionality, the following methods will be useful on the BLOCK Phasable.
method done
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
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
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
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
The LOOP Phasable
method done
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
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
method fine(Exception @exceptions --> Bool)
Did the LOOP exit fine?
This is the same as BLOCK.fine (but on LOOP).
The ROUTINE Phasable
Same as BLOCK, but with the following exception:
method entry-success
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 {} }
once-per
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
throw/rethrow
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).
New Features Available
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.
Alternative Ideas
Things that might need changing are:
- If we also need an
is ASYNCmodifier to declare a Phaser asynchronous, that's an option too.
Conclusion
Hopefully this will provide a starting point for a discussion about phasers, and how they might be made easier to work with.
Authors
- Tim Nelson: original document (0.1) & 1.0 rewrite
- Clifton Wood: syntax improvements
- yary: suggestion about LOOP
- Vadim Belman (vrurg): Extensive criticism of early versions of the propsal, resulting in a rewrite of the majority of it
Appendix A: Phasers in the Phaser Definition Syntax
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.