Raku / problem-solving

🦋 Problem Solving, a repo for handling problems that require review, deliberation and possibly debate
Artistic License 2.0
70 stars 16 forks source link

More flexible Phasers #308

Open wayland opened 2 years ago

wayland commented 2 years ago

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:

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.

wayland commented 2 years ago

Initial Proposal

Attached is a slightly modified version of the document I posted to the perl6-users mailing list.

PhasersMkIIDocumentation.pdf

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!

vrurg commented 2 years ago

@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.

wayland commented 2 years ago

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:

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:

method entry-success

method entry-success(--> Bool)

Was the the block successfully entered?

Returns True. Will be overridden by:

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:

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:

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:

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)
wayland commented 2 years ago

@vrurg

Thanks for your suggestions.

vrurg commented 2 years ago

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 see ENTER/LEAVE (or IN/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.

wayland commented 2 years ago

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:

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 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,

jnthn commented 2 years ago

I don't have time to look closely at this and critique it, but will note that:

  1. 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 for CHECK and "check time".
  2. Any proposal that wants to eliminate widely used existing syntax is not likely to go anywhere.
wayland commented 2 years ago

@jnthn

  1. That probably should have been obvious (but it wasn't to me!) Thanks very much for this info.
  2. 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!

Xliff commented 2 years ago

@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.

vrurg commented 2 years ago

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.

vrurg commented 2 years ago
  1. 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 for CHECK 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.

wayland commented 2 years ago

@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:

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,

vrurg commented 2 years ago

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 the ENTER queue, but one thrown from a LEAVE phaser will not. The exceptions thrown by failing PRE and POST phasers cannot be caught by a CATCH in the same block, which implies that POST phaser are not run if a PRE 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.

wayland commented 2 years ago

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 .

wayland commented 2 years ago

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:

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:

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:

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:

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:

Conclusion

Hopefully this will provide a starting point for a discussion about phasers, and how they might be made easier to work with.

Authors

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) 
    }
}
FCO commented 2 years ago

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?

lizmat commented 2 years ago

@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.

FCO commented 2 years ago

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.