matatu blog

Brian writes about computing

August 2, 2018

Ping Pong in Perl 6

Message passing between threads using channels is a simple way to manage concurrency.

A friend recently showed me a cool program in elixir to demonstrate message passing -- two threads count by passsing ascending numbers back and forth. A quick search turns up a handful of examples.

I decided to implement this in Perl 6. Here's what I came up with →

$ping and $pong are each a Channel. The subroutines ping and pong receive from their corresponding channels and send to the other one.

If you run this, you'll see the numbers from one to five.

ping 1 (thread #3)
pong 2 (thread #4)
ping 3 (thread #3)
pong 4 (thread #4)
ping 5 (thread #3)

my ($ping, $pong) = Channel.new xx 2;

sub ping {
   while $ping.receive -> $n {
       say "ping $n (thread #{$*THREAD.id})";
       $pong.send: $n + 1;
   }
}

sub pong {
   while $pong.receive -> $n {
       last if $n >= 5;
       say "pong $n (thread #{$*THREAD.id})";
       $ping.send: $n + 1;
   }
}

$ping.send: 1;
await Promise.anyof(
 (start ping),
 (start pong)
);


Next I wanted to encapsulate some common behavior. It's a ping-pong "player" that's sending and receiving -- so let's make a Player class.

I imagine "ping" and "pong" to be the sounds made by each of the player's paddles, as they hit the ball, so I added a $.sound attribute. And an $.opponent attribute to represent, well, each player's opponent.

$.channel is another attribute which handles (i.e. method delegation) the send and receive methods.

Running this gives us something similar to the last one:

ping 1 (thread #3)
pong 2 (thread #4)
ping 3 (thread #3)
pong 4 (thread #4)
ping 5 (thread #3)
class Player {
    has $.sound;
    has $.opponent is rw;
    has $.channel handles <send receive> = Channel.new;
    has $.promise;
    method play {
        $!promise =
         start {
            while self.receive -> $n {
                last if $n == 6;
                say "$.sound $n (thread #{$*THREAD.id})";
                $.opponent.send: $n + 1;
            }
        }
    }
}

my ($ping,$pong) = <ping pong>.map: { Player.new(:$^sound) }
($ping, $pong)>>.opponent = ($pong, $ping);
($ping, $pong)>>.play;
$ping.send: 1;
await $pong.promise;

Now let's make this a bit more like ping pong:
  • There is a 1 in 4 chance of missing the ball.
  • The first one to eleven wins, you have to win by two.
  • Every five points, switch who serves.
ping 1 pong 2 ping 3 pong 4 ping 5 pong 6 ping 7 Miss by pong. Score: 1 to 0
ping 1 pong 2 ping 3 pong 4 ping 5 pong 6 ping 7 Miss by pong. Score: 2 to 0
ping 1 pong 2 ping 3 pong 4 ping 5 pong 6 ping 7 pong 8 ping 9 Miss by pong. Score: 3 to 0
ping 1 Miss by pong. Score: 4 to 0
ping 1 pong 2 ping 3 Miss by pong. Score: 5 to 0
--Now serving: pong--
Miss by pong. Score: 6 to 0
pong 1 ping 2 pong 3 ping 4 Miss by pong. Score: 7 to 0
pong 1 ping 2 Miss by pong. Score: 8 to 0
pong 1 ping 2 Miss by pong. Score: 9 to 0
pong 1 Miss by ping. Score: 9 to 1
--Now serving: ping--
Miss by ping. Score: 9 to 2
ping 1 Miss by pong. Score: 10 to 2
ping 1 Miss by pong. Score: 11 to 2

Winner: ping
Final Score: 11 to 2
Some comments and things to notice:
  • $.sound handles stringification.
  • If the promise was Kept, the loop exited, so a player missed.
  • >> is the same as » -- this is like map.
  • Adding another entry to @sounds would set up a circle of message passing.
class Player {
    has $.sound handles 'Str';
    has $.opponent is rw;
    has $.channel handles <send receive> = Channel.new;
    has $.promise;

    method missed {
        $.promise.status == Kept;
    }
    method play {
        $!promise = start {
            while self.receive -> $n {
                last if (^4).pick==1;
                print "{$.sound} $n ";
                self.opponent.send: $n + 1;
            }
        }
    }
}

my @sounds = <ping pong>;
my @players = @sounds.map: { Player.new(:$^sound) }
@players».opponent «=» @players.rotate;
my $serving = @players[0];
my %score = @players Z=> 0 xx *;

@players».play;
repeat {
  $serving.send(1);
  await Promise.anyof(@players».promise);
  with @players.first({ .missed }) -> $missed {
      print "Miss by $missed.";
      %score{$missed.opponent}++;
      $missed.play;
  }
  say " Score: " ~ %score{@sounds}.join(' to ');
  if %score.values.sum %% 5 {
      $serving .= opponent;
      say "--Now serving: $serving--";
  }
} until %score.values.any >= 11 and (abs [-] %score.values) >= 2;

say "\nWinner: " ~ %score.invert.Hash{ %score.values.max };
say "Final Score: " ~ %score{@sounds}.join(' to ');

Conclusion and Further thoughts

  • Channels are a nice way to do message passing between threads.
  • Channels and Promises can be attributes of objects.
  • This is handy for encapsulatation of message passing.