6

I am trying to write an observer pattern using a timer class which invoke its subscribers at a given interval. All while trying to understand the concept of promises in Perl6.

class Timer does Observable
{
    has Promise $!p;
    has Int $!interval;
    has Bool $!active = False;

    submethod BUILD(Int :$!interval) {}

    method start {
        $!p = Promise.start( {
            $!active = True;
            loop {
                sleep($!interval);
                @!action_listeners>>.actionPerformed(ActionEvent.new);
                if !$!active {$!p.keep}
            }
        });
    }

    method stop {
        $!active = False;
    }
}

The Observer role simply just have an array with subscribers. Granted I should have made a method for updating all subscribers within the role.

role Observable {
    has ActionListener @!action_listeners;

    method addActionListener(ActionListener $al){
        @!action_listeners.push($al);
    }

    method removeActionListener{
        @!action_listeners.pop;
    }
}

The ActionListener role simply has a actionPerformed method. The class ActionEvent might not be nessesary, it is simply an empty class at the moment.

role ActionListener
{
    method actionPerformed(ActionEvent $e) { ... }
}

Running from a script:

my Subscriber $s = Subscriber.new;
my Timer $t = Timer.new(interval => 1);

$t.start;
$t.addActionListener($s);
$t.addActionListener(Subscriber.new);

.... #doing stuff that lasts for a while
$t.stop;

The subscriber class implements the ActionListener role (has a method called actionPerformed).

Though this work fine: The actionPerformed method of the subscribers get invoked until I call the stop method of the timer. While having no proper way to remove a given subscriber. Also I was wondering if there is a better way to keep/break a promise, from outside given the code loops infinitively.

I am basically wondering if I might be missing out on built in features in Perl6? Am I reinventing the wheel?

Mikkel
  • 575
  • 2
  • 6
  • Can you add the code for `ActionListener` if it's relevant? Also, `loop` loops forever... – jjmerelo Jun 28 '18 at 09:35
  • Sure ActionListener is a simple role with one method defined (actionPerformed) . The way breaking out of the loop, is by keeping the promise (by setting $!active to True). Agreed it is not a particularly nice way of stopping it. Are you suggesting that the loop is still running even though the Promise invoking it, has been kept? – Mikkel Jun 28 '18 at 09:49
  • not really. I hadn't seen you wer doing action-at-a distance setting a class variable from other function. Why don't you use stop to keep the promise, instead of setting that variable? Can't you keep the Promise also if there are no more tasks to listen? – jjmerelo Jun 28 '18 at 09:55
  • Ending the promise when the array @!action_listners.elems == 0, could certainly be an option. Though then starting the promise before adding actionlisteners, would end it immediately. – Mikkel Jun 28 '18 at 10:30
  • then you really have to look at what you need to do. Maybe [channels](new-blob) are better options than that; in fact, it's the best if you want to share data structure across threads. Promises are better for asynchronous I/O, I think. – jjmerelo Jun 28 '18 at 11:06
  • 1
    `my $p = Promise.new; $p.then({ say 2 }); say 1; $p.keep;` `1␤2␤` If you really want to learn about Promises in Perl 6, forget about the `start` method for the moment, as it is really just shorthand for: `method start (&code) { my $p = Promise.new; $*SCHEDULER.cue({$p.keep(code())}); $p }` (There are some differences, but this is basically what it does) – Brad Gilbert Jun 28 '18 at 15:57
  • You will indeed have to terminate the loop in addition to keeping or breaking the promise. However, I would expect that when you leave the start block, it'll try to keep the promise for you and fail because it has already been kept. So I'd just replace the `$!p.keep` with `break`. – timotimo Jun 28 '18 at 18:18
  • In my previous comment I claimed that you don't only have to keep the promise but also exit the loop, but it occurred to me that `start` will return a `Promise` and take the `Vow`, which means that you can't call `.keep` or `.break` on the `Promise`, only on the corresponding `Vow`, which the scheduler "owns". Just replace `$!p.keep` with `last` in order to leave the loop and that should be fine. – timotimo Jun 28 '18 at 18:27

2 Answers2

9

To answer the question of re-inventing the wheel even further: You should have a look at Supply. Supply is the Perl 6 term for Observable, and Tap is the Perl 6 term for Subscription.

In order to get an observable that invokes the subscribers in a regular interval, you can use the interval class method of the Supply type. You would call .tap on the result of the .interval call to register a code block to be run whenever the Supply emits a value and the result of the .tap call is the Tap object that you can close the subscription with.

These talk slides by Jonathan Worthington might be interesting if you want to know more about Supplies.

Elizabeth Mattijsen
  • 25,654
  • 3
  • 75
  • 105
timotimo
  • 4,299
  • 19
  • 23
7

To answer your question "Am I reinventing the wheel?". Possibly.

You can actually ask the current ThreadPoolScheduler (aka $*SCHEDULER) to cue a piece of code to be run at an interval:

my $canceller = $*SCHEDULER.cue( { say $++ }, every => 0.2 );
sleep 2;
# 0 1 2 3 4 5 6 7 8 9
$canceller.cancel;
say "stopped";  # no numbers should be seen afterward
# stopped    
sleep 1;        # keep from exiting immediately

This does not involve any Promises whatsoever, so if your goal is to know more about Promises, then this is not the way to go. If you want to create an observer pattern, this might be the cleanest way of creating it.

Elizabeth Mattijsen
  • 25,654
  • 3
  • 75
  • 105