Beefy Boxes and Bandwidth Generously Provided by pair Networks
There's more than one way to do things
 
PerlMonks  

Re^4: a State machine with Roles - possible? (class or instance)

by mascip (Pilgrim)
on May 30, 2013 at 14:27 UTC ( [id://1036063]=note: print w/replies, xml ) Need Help??


in reply to Re^3: a State machine with Roles - possible? (class or instance)
in thread a State machine with Roles - possible?

Thanks for the discussion LanX :-) I think that you're looking for something too complicated, and that Roles just do the trick. (i might well be wrong). Here is the Perl code for getting the same behavior as the Ruby code from the slides (it could all be shorter and put in the same file, with MooseX::Declare - but some people might not have it installed):

test.pl

use feature ':5.16'; use strictures; use Door; use Moose::Util qw( apply_all_roles ); my $main_door = Door->new; # The Door is Closed $main_door->knock; # knock knock # Open it apply_all_roles($main_door, 'Opened'); $main_door->knock; # just come on in! # Close it apply_all_roles($main_door, 'Closed'); $main_door->knock; # knock knock # Bug ?? say 'Why does this not print ??' if $main_door->DOES('Closed');

Door.pm

package Door; use Moo; use feature ':5.16'; use Moose::Util qw( apply_all_roles ); sub BUILD { apply_all_roles($_[0], 'Closed'); # default State at construction } sub knock { say 'DEFECT: a door should never be nor opened nor closed'; # This method will be overriden by the Roles # (should never be printed) } 1;

Closed.pm

package Closed; use Moose::Role; use feature ':5.16'; sub knock { say 'knock knock'; } 1;

Opened.pm

package Opened; use Moose::Role; use feature ':5.16'; sub knock { say 'just come one in!'; } 1;

Which does what is expected (output written as comments in test.pl).

But it has several problems:

  • To check whether a Door is closed or opened, i need to check if the object DOES any of these two Roles. Unfortunately, DOES seems to not work, for a Role instanciated on an object. This might be a bug in Moose::Object::DOES. (see the last line of test.pl)
  • When a door is Closed, it is still Opened at the same time. I need to be able to unapply_role(). Which is what they do in the Ruby example with 'unmixin'. And it's my initial question in this thread: how to do it in Perl?

If there were simple solutions to these two problems, then i think we could implement complex State machines with multidimensional states.

There is here someone who asked pretty much the same question as me, with a pretty good example of multidimensional State machine: a warrior who can be turned into a Zombie, poisoned, made stronger, turned into a Chipmunk, or all of these at the same time. The answer to that person's question was an alternative solution with aspects. I would still prefer the solution with Roles, which are more flexible, and in my opinion clearer.

Was that clear? Helpful? Did i miss your point LanX?

Replies are listed 'Best First'.
Re^5: a State machine with Roles - possible? (class or instance)
by salva (Canon) on May 30, 2013 at 15:48 UTC
    then i think we could implement complex State machines with multidimensional states.

    Well, actually, just a small subset. Those whose response to an event may only depend on one dimension of the state. Let's call them linear multidimensional state-machines. Those are pretty uninteresting, and can be simulated easily by an aggregation of unidimensional state-machines.

    In practice, you will find that there are interactions between the state dimensions and so, in order to model the responses to the different events that may array you will have to take into consideration several dimensions. In other words, behavior can be modeled as a multidimensional matrix of responses.

    My experience is that when one gets to the point of needing a multi-dimensional non-linear state machines to solve some problem* it just means that he is doing something wrong and needs to get back and try to break the problem in simpler ones.

    *) unless there is some kind of code generator doing it!

      Thank you Salva, i think that's a very good point: if you need to do this, you probably need to redesign.

      I think your first point is wrong though: you can have "non-linear" behavior, for example a "Person" turned into a "Zombie", could become stronger when they are "Poisoned" (instead of becoming weaker like a non-Zombie person).

      And transitions can be implemented in the Roles, so that you can also have complex transitions.

      So, i think you'd get a truly multidimensional State Machine (is that right, or did i misunderstand something?). It might just get a bit messy in terms of the code, which shows you might want to redesign.

      It's still nice to know that it's possible though. In the meantime i'll try and use Class::StateMachine.

        I think your first point is wrong though: you can have "non-linear" behavior, for example a "Person" turned into a "Zombie", could become stronger when they are "Poisoned" (instead of becoming weaker like a non-Zombie person).

        You are right in the sense that it can be done. But my point is that you loose the advantages of considering the state as multidimensional.

        For me, the real issue with complex state machines (mutidimensional or not) is not how to implement them but how to express them in a way that is easy to understand for humans. How to avoid ending with a big ball of code.

Re^5: a State machine with Roles - possible? (class or instance)
by james2vegas (Chaplain) on May 30, 2013 at 20:31 UTC
    # Bug ?? say 'Why does this not print ??' if $main_door->DOES('Closed');

    This doesn't work because you're mixing Moo and Moose in the same package, not sure if that counts as a bug, but using Moose consistently (or Moo) makes DOES work properly.

      Indeed, now i got it working!

      Now i'm struggling with un-applying the Role, and making a 2D state exmaple (Open/Closed, Unlocked/Locked). I'm stopping for now, might finish on Monday if i feel like it. Or i might just stop as it's not a priority.

Re^5: a State machine with Roles - possible? (class or instance)
by james2vegas (Chaplain) on May 30, 2013 at 20:57 UTC
    To unapply (which doesn't seem to be supported in Moose nor Moo at the moment), presuming you have access to the original roles applied:
    • find the superclass of the object (this will be the original class of the object without the roles applied)
    • subtract the roles to unapply from the list of roles applied
    • re-bless the object into its original class (in Moo, this is only necessary if the roles list is empty, as you use the class name in the next step)
    • if the roles list isn't empty, create a new role application with the object's original class and new list of roles,
      bless the object into this new class (in Moose, apply roles)

      Like i said to Salva above, in most cases one can (and probably should) achieve most things with Role attributes. But still, i wanted to un-apply a Role, to see if it's possible.

      And with your help james2vegas, I managed to do it, thank you!

      Well, it's very dirty and i wouldn't use this code in a real project. I cheated because my object's class name is not 'Door' anymore: it's an anonymous class. And I don't know how to retrieve all the class names (if there were several). So, i passed the class name as an attribute to the unapply_role() function.

      Well, i'm pretty sure that re-blessing a Moose object is a bad idea anyway... but it's nice to play with things and at least get a simple example working :-)

      Here is the result:

      package MASCIP::Utils::Roles; use version; our $VERSION = qv('0.0.1'); use Perl6::Export::Attrs; use Moose::Util qw( ensure_all_roles ); ## List of Role names that an object (instance) composes sub role_names_of :Export(:DEFAULT) { return [ split('\|', $_[0]->meta->roles->[0]->name) ]; } ## Switches one Role for another # @params: old Role name to un-apply, new Role name to apply sub switch_role :Export(:DEFAULT) { my ($object, $class_of_object, $old, $new) = @_; # Un-apply old Role unapply_role($object, $class_of_object, $old); # Apply new Role my @new_roles = @{ role_names_of($object) }; push @new_roles, $new; _rebless_and_apply_new_roles($object, $class_of_object, @new_roles +); } ## Un-apply a Role from an object (instance) sub unapply_role :Export(:DEFAULT) { my ($object, $class_of_object, $role_name) = @_; # the class is used to rebless my @new_roles = grep { $_ ne $role_name } @{ role_names_of($object) }; _rebless_and_apply_new_roles($object, $class_of_object, @new_roles +); } ## Gets rid of all old Roles, and apply a list of new ones sub _rebless_and_apply_new_roles { my ($object, $class_of_object, @new_roles) = @_; bless $object, $class_of_object; # THIS IS BAD !!! # But it works for this simple e +xample ensure_all_roles($object, @new_roles); } 1;
      It works/ To improve it one would need to retrieve the object class(es) from it's meta-object.

      Here are my improved Doors:
      Door.pm

      use MooseX::Declare; ## Door behaviour: # - you cannot lock an Open door # - you cannot open a Locked door class Door { use MyApp::Bundle; has name => ( is => 'ro', isa => 'Str', required => 1 ); # Default State at construction: Closed and Unlocked method BUILD (@_) { apply_all_roles($self, 'Closed', 'Unlocked') } # THESE WON'T WORK right now, because the Role must be un-applied! + method is_opened { $->DOES('Opened') } method is_closed { $->DOES('Closed') } method is_locked { $->DOES('Locked') } method is_unlocked { $->DOES('Unlocked') } method current_roles_names { return [ split('\|', $->meta->roles->[0]->name) ]; } method current_state { my $openness = $->is_opened ? 'OPENED' : 'CLOSED'; my $lockness = $->is_locked ? 'LOCKED' : 'UNLOCKED'; warn 'DEFECT: a Door should never be opened and locked !!!' if $->is_opened && $->is_locked; return [ $openness, $lockness ]; } method to_str { return $->name . ': ' . join(', ', @{$->current_state}) } } # END CLASS Door role Opened { use MyApp::Bundle; method knock { say "don't knock: it's opened" } method open { carp 'already opened' } method close { switch_role($self, 'Door', 'Opened', 'Closed') } } # END ROLE Opened role Closed { use MyApp::Bundle; method close { carp 'already closed' } method open { # Cannot lock an open door if ($->is_locked) { say 'you cannot open a locked door'; return; } switch_role($self, 'Door', 'Closed', 'Opened'); } } # END ROLE Opened role Locked { use MyApp::Bundle; method lock { carp 'already locked' } method unlock { switch_role($self, 'Door', 'Locked', 'Unlocked') } } # END ROLE Locked role Unlocked { use MyApp::Bundle; method lock { if ($->is_opened) { carp 'you cannot lock an open door'; return; } switch_role($self, 'Door', 'Unlocked', 'Locked') } method unlock { carp 'already unlocked' } } # END ROLE Unlocked
      As well as the module bundle that i use everywhere:
      MyApp::Bundle
      ### Modules used in all other classes package MyApp::Bundle; use Syntax::Collector -collect => q/ use MASCIP::Utils::Roles 0.0.1; use Method::Signatures::Modifiers 20130505; use strictures 1.004004; use feature 1.27 qw( :5.16 ); use Carp 1.26 qw( carp ); use Moose::Util 2.0802 qw( apply_all_roles ); use invoker 0.34; /; 1;
      And the test:
      use Door; use MyApp::Bundle; my $front = Door->new(name => 'Front'); my $back = Door->new(name => 'Back'); sub say_doors_state { say ' ' . $front->to_str . ' | ' . $back->to_str; say ''; } say 'START in the house...'; say '...the doors are Closed and Unlocked (default)'; say_doors_state; # Front: CLOSED, UNLOCKED | Back: CLOSED, UNLOCKED say 'Lock the Front door'; $front->lock; say_doors_state; # Front: CLOSED, LOCKED | Back: CLOSED, UNLOCKED say 'Try to open the Front door (impossible)'; $front->open; say_doors_state; # you cannot open a locked door # Front: CLOSED, LOCKED | Back: CLOSED, UNLOCKED say 'Open the Back door'; $back->open; say_doors_state; # Front: CLOSED, LOCKED | Back: OPENED, UNLOCKED say 'Try to lock the Back door (impossible)'; $back->lock; say_doors_state; # you cannot lock an open door at test.pl line 26. # Front: CLOSED, LOCKED | Back: OPENED, UNLOCKED say 'Close the Back door'; $back->close; say_doors_state; # Front: CLOSED, LOCKED | Back: CLOSED, UNLOCKED say 'Lock the Back door'; $back->lock; say_doors_state; # Front: CLOSED, LOCKED | Back: CLOSED, LOCKED say "I'm safe!";

      That's it. Suggestions welcome.

      Edit: Nothing, i wrote something wrong
Re^5: a State machine with Roles - possible? (class or instance)
by LanX (Saint) on May 30, 2013 at 14:44 UTC
    I'm neither a Moose nor a Ruby expert, but I think with this Ruby code one can instantiate multiple objects each with individual states.

    But since Moose roles operate on classes you'll be restricted on a singleton class, i.e. just one object (resp. all objects with the same state.)

    But again theoretical thoughts, maybe better if I refrain from this subject.

    But thanks for bringing it up, I learned a lot.

    Cheers Rolf

    ( addicted to the Perl Programming Language)

    PS: I just remember that I once had a similar requirement which I solved with a state attribute.

    So calling $self->{state}{method}() solved it for me and I was able to have multidimensional states per object.

    Thats very similar to the already proposed proxy-methods.

      While it is true that only one class (package) is created for every unique combination of class and role to apply, that doesn't limit those objects to be singletons, they would each have their own state, shared with the roles applies to them.
        I'm not sure if you are getting my point.

        Let's say I want to model a house with 100 doors.

        The Ruby code allows to call 100 times new() from class "Door" to create 100 door objects with independent states, because the methods of the mixins belong to the objects.

        The shown Moose code OTOH needs 100 packages achieve this, cause all objects of such a package share the same roles. This doesn't sound very handy or scalable.

        And I couldn't see any code to generate such classes, hence we have to manually name them Door1, Door2, ...Door100.

        Plz correct me if I'm wrong.

        Cheers Rolf

        ( addicted to the Perl Programming Language)

      • UPDATE 01 Jun 2013

        oops, I just realized that you already explained the mechanism behind apply_all_roles further up this thread. Sorry, I wasn't aware we are talking about the same approach.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: note [id://1036063]
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others imbibing at the Monastery: (4)
As of 2024-03-28 06:38 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found