Please consider the code below. Why is the output of this is "BABABA" and not "AAABAA" / "AABAAAB"? Shouldn't the two supplies run in parallel and the whenever fire immedeatly when there is an event in any of them?
my $i = 0;
my $supply1 = supply { loop { await Promise.in(3); done if $i++> 5; emit("B"); } };
my $supply2 = supply { loop { await Promise.in(1); done if $i++> 5; emit("A"); } };
react
{
#whenever Supply.merge($supply1, $supply2) -> $x { $x.print }
whenever $supply1 -> $x { $x.print };
whenever $supply2 -> $x { $x.print };
}
When we subscribe to a supply
block, the body of that supply
block is run immediately in order to set up subscriptions. There's no concurrency introduced as part of this; if we want that, we need to ask for it.
The best solution depends on how close the example is to what you're doing. If it's very close - and you want to emit
values every time interval - then the solution is to use Supply.interval
instead:
my $i = 0;
my $supply1 = supply { whenever Supply.interval(3, 3) { done if $i++ > 5; emit("B"); } };
my $supply2 = supply { whenever Supply.interval(1, 1) { done if $i++> 5; emit("A"); } };
react {
whenever $supply1 -> $x { $x.print };
whenever $supply2 -> $x { $x.print };
}
Which simply sets up a subscription and gets out of the setup, and so gives the output you want, however you do have a data race on the $i
.
The more general pattern is to just do anything that gets the loop happening out of the setup step. For example, we could use an a kept Promise
to just "thunk" it:
my constant READY = Promise.kept;
my $i = 0;
my $supply1 = supply whenever READY {
loop { await Promise.in(3); done if $i++> 5; emit("B"); }
}
my $supply2 = supply whenever READY {
loop { await Promise.in(1); done if $i++> 5; emit("A"); }
}
react {
whenever $supply1 -> $x { $x.print };
whenever $supply2 -> $x { $x.print };
}
Which helps because the result of a Promise
will be delivered to the supply
block via the thread pool scheduler, thus forcing the execution of the content of the whenever
- containing the loop - into its own scheduled task.
This isn't especially pretty, but if we define a function to do it:
sub asynchronize(Supply $s) {
supply whenever Promise.kept {
whenever $s { .emit }
}
}
Then the original program only needs the addition of two calls to it:
my $i = 0;
my $supply1 = supply { loop { await Promise.in(3); done if $i++> 5; emit("B") } }
my $supply2 = supply { loop { await Promise.in(1); done if $i++> 5; emit("A") } }
react {
whenever asynchronize $supply1 -> $x { $x.print }
whenever asynchronize $supply2 -> $x { $x.print }
}
To make it work as desired. Arguably, something like this should be provided as a built-in.
It is possible to use a Channel
as well, as the other solution proposes, and depending on the problem at hand that may be suitable; the question is a bit too abstracted from a real problem for me to say. This solution stays within the Supply
paradigm, and is neater in that sense.
Ok, so here is my real code. It seems to work, but I think there is a race condition somewhere. Here's some typical (albeit short) output.
A monster hatched.
A monster hatched.
A hero was born.
The Monster is at 2,3
The Monster is at 3,2
The Player is at 0,0
The Monster (2) attacks the Player (3)
The Monster rolls 14
The Player rolls 4
The Monster inflicts 4 damage
The Player (3) attacks the Monster (2)
The Player rolls 11
The Monster rolls 8
The Player inflicts 45 damage
The Monster is dead
The Monster is at -3,-3
The Player is at 4,-3
The Monster (1) attacks the Player (3)
The Monster rolls 8
The Player rolls 5
The Monster inflicts 11 damage
The Player has 32 hitpoints left
The Monster is at -4,1
The Player is at -1,4
The Player (3) attacks the Monster (1)
The Player rolls 12
The Monster rolls 11
The Player inflicts 46 damage
The Monster is dead
Stopping
Game over. The Player has won
Now the strange thing is, sometimes, in maybe 20% of the runs, the last line of the output is
Game over. The GameObject has won
as if the object got caught while it already is partially deconstructed? Or something? Anyway here's the code.
class GameObject
{
has Int $.id;
has Int $.x is rw;
has Int $.y is rw;
has $.game;
has Int $.speed; #the higher the faster
has Bool $.stopped is rw;
multi method start( &action )
{
start {
loop {
&action();
last if self.stopped;
await Promise.in( 1 / self.speed );
}
$.game.remove-object( self );
}
}
method speed {
$!speed +
# 33% variation from the base speed in either direction
( -($!speed / 3).Int .. ($!speed / 3).Int ).pick
;
}
}
role UnnecessaryViolence
{
has $.damage;
has $.hitpoints is rw;
has $.offense;
has $.defense;
method attack ( GameObject $target )
{
say "The {self.WHAT.perl} ({self.id}) attacks the {$target.WHAT.perl} ({$target.id})";
my $attacker = roll( $.offense, 1 .. 6 ).sum;
say "The {self.WHAT.perl} rolls $attacker";
my $defender = roll( $target.defense, 1 .. 6 ).sum;
say "The {$target.WHAT.perl} rolls $defender";
if $attacker > $defender
{
my $damage = ( 1 .. $.damage ).pick;
say "The {self.WHAT.perl} inflicts {$damage} damage";
$target.hitpoints -= $damage ;
}
if $target.hitpoints < 0
{
say "The {$target.WHAT.perl} is dead";
$target.stopped = True;
}
else
{
say "The {$target.WHAT.perl} has { $target.hitpoints } hitpoints left";
}
}
}
class Player is GameObject does UnnecessaryViolence
{
has $.name;
multi method start
{
say "A hero was born.";
self.start({
# say "The hero is moving";
# keyboard logic here, in the meantime random movement
$.game.channel.send( { object => self, x => (-1 .. 1).pick, y => (-1 .. 1).pick } );
});
}
}
class Monster is GameObject does UnnecessaryViolence
{
has $.species;
multi method start
{
say "A monster hatched.";
self.start({
# say "The monster {self.id} is moving";
# AI logic here, in the meantime random movement
$.game.channel.send( { object => self, x => (-1 .. 1).pick, y => (-1 .. 1).pick } );
});
}
}
class Game
{
my $idc = 0;
has GameObject @.objects is rw;
has Channel $.channel = .new;
method run{
self.setup;
self.mainloop;
}
method setup
{
self.add-object( Monster.new( :id(++$idc), :species("Troll"), :hitpoints(20), :damage(14), :offense(3), :speed(300), :defense(3), :x(3), :y(2), :game(self) ) );
self.add-object( Monster.new( :id(++$idc), :species("Troll"), :hitpoints(10), :damage(16), :offense(3), :speed(400), :defense(3), :x(3), :y(2), :game(self) ) );
self.add-object( Player.new( :id(++$idc), :name("Holli"), :hitpoints(50), :damage(60), :offense(3), :speed(200) :defense(2), :x(0), :y(0), :game(self) ) );
}
method add-object( GameObject $object )
{
@!objects.push( $object );
$object.start;
}
method remove-object( GameObject $object )
{
@!objects = @!objects.grep({ !($_ === $object) });
}
method mainloop
{
react {
whenever $.channel.Supply -> $event
{
self.stop-game
if self.all-objects-stopped;
self.process-movement( $event );
self.stop-objects
if self.game-is-over;
};
whenever Supply.interval(1) {
self.render;
}
}
}
method process-movement( $event )
{
#say "The {$event<object>.WHAT.perl} moves.";
given $event<object>
{
my $to-x = .x + $event<x>;
my $to-y = .y + $event<y>;
for @!objects -> $object
{
# we don't care abour ourselves
next
if $_ === $object;
# see if anything is where we want to be
if ( $to-x == $object.x && $to-y == $object.y )
{
# can't move, blocked by friendly
return
if $object.WHAT eqv .WHAT;
# we found a monster
.attack( $object );
last;
}
}
# -5 -1 5
# we won the fight or the place is empty
# so let's move
.x = $to-x == 5 ?? -4 !!
$to-x == -5 ?? 4 !!
$to-x;
.y = $to-y == 5 ?? -4 !!
$to-y == -5 ?? 4 !!
$to-y;
}
}
method render
{
for @!objects -> $object {
"The {$object.WHAT.perl} is at {$object.x},{$object.y}".say;
}
}
method stop-objects
{
say "Stopping";
for @!objects -> $object {
$object.stopped = True;
}
}
method stop-game {
"Game over. The {@!objects[0].WHAT.perl} has won".say;
$.channel.close;
done;
}
method game-is-over {
return (@!objects.map({.WHAT})).unique.elems == 1;
}
method all-objects-stopped {
(@!objects.grep({!.stopped})).elems == 0;
}
}
Game.new.run;
Supplies are asynchronous, not concurrent. You will need to use channels instead of supplies to feed them concurrently.
use v6;
my $i = 0;
my Channel $c .= new;
my $supply1 = start { for ^5 { await Promise.in(1); $c.send("B"); } };
my $supply2 = start { for ^5 { await Promise.in(0.5); $c.send("A"); } };
await $supply2;
await $supply1;
$c.close;
.say for $c.list;
In this case, the two threads start at the same time, and instead of using .emit
, then .send
to the channel. In your example, they are effectively blocked while they wait, since they are both running in the same thread. They only give control to the other supply after the promise is kept, so that they run apparently "in parallel" and as slow as the slower of them.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With