Finite state machines in Ruby

    The article is the authorship of the Habrauser preprocessor , which could not publish it for all obvious reasons. So all the pluses to him :)

    Finite-state machine is such a thing that describes the behavior of an object with a finite number of states. Ways of transition from one state to another, the conditions of this transition, actions performed during the transition or after. I've always had a bad time with theory, so I won’t go into it anymore; instead, for those who are interested in details, I can recommend watching Wikipedia (as without it) http://en.wikipedia.org/wiki/Finite-state_machine and http://ru.wikipedia.org/wiki/Final_automaton, and from there already drip as much as you want. In practice, this can be used a lot where, from parsing strings (hello Ragel ), to the User model in your web application.

    Now I want to talk about the implementation of state machine in ruby ​​language. There is such a wonderful site ruby-toolbox.com , by which you can fairly accurately judge what is now popular in the world of rubies. In the State machines section, in the first place we see the gem aasm from rubyist. By the way, it just so happened that the quality of a ruby ​​library can almost always be judged by its popularity, in any case, in areas where there is library competition. Well, so it happened. aasm is really good, unlike its popular predecessor ( acts_as_state_machine) can work not only (and for some, not so much) with ActiveRecord, but also with any ruby-object. That's just the documentation for it is very scarce, even on the western web I could not find any more or less complete description of this library. So I will allow myself, in essence, to write a small manual for it.

    So, let's start with an example from the library itself (this is all the documentation).

    class Conversation < ActiveRecord::Base
    include AASM

    aasm_initial_state :unread

    aasm_state :unread
    aasm_state :read
    aasm_state :closed

    aasm_event :view do
    transitions :to => :read, :from => [:unread]
    end

    aasm_event :close do
    transitions :to => :closed, :from => [:read, :unread]
    end
    end



    What is now generated for us: If the object is inherited from ActiveRecord :: Base, then the persistence component of aams is mixed in with it. It is for her that bang methods are relevant in the first place. conversation.view! not only translates the current state of the object, but also saves it in the database. Also, no one bothers you to define aasm_write_state for any object and do everything in it that the soul desires (just like aasm_read_state). Let's look at a couple more examples. What we see. First callbacks. For transition, these are: guard and: on_transition. If: guard true, then the transition will fail; if not, then no. : on_transition is executed during the transition. For example, this means that you cannot go to the next state in this callback.

    conversation = Conversation.new

    conversation.aasm_current_state => :unread

    conversation.view # перейти в состояние :read

    conversation.view! # перейти в состояние :read и вызвать aasm_write_state, если он определен
    conversation.read? # true or false. Мы как бэ спрашиваем “текущее состояние read?”

    conversation.closed # Генерируются named scopes для всех состояний, соответсвенно этот метод вернет нам scope для всех закрытых бесед.






    aasm_state :waiting, :enter => :start_timer
    aasm_state :selecting_cards
    aasm_state :made_turn, :exit => lambda { unseletcted_cards.each { |c| c.destroy }

    aasm_event :go do
    transitions :to => :selecting_cards, :from => [:ready], :guard => :attacking?
    transitions :to => :waiting, :from => [:ready], :guard => :defending?
    end

    aasm_event :make_turn, :success => :after_make_turn do
    transitions :to => :made_turn, :from => [:selecting_cards], :on_transition => :do_make_turn
    end




    Event -: success, which is executed after the transition is completed successfully.
    In state -: enter and: exit, they are executed, respectively, upon entering and exiting the state (it does not matter through which event and through which transition).
    Any of these callbacks can be either Symbol or Proc, in general, like everywhere else.

    The object itself has aasm_event_fired and aasm_event_failed. If one of them is defined on the object, then aasm_event_failed will be called with one parameter (the name of the event), and aasm_event_fired with two parameters (the name of the event and the name of the state in which the object was transferred).

    From this example we also see that the event can any number of transitions to be determined. It will be executed the one with: from matches the current state, and: guard returns true.

    That’s basically all. Here is an example of a small, but very flexible and expandable ruby ​​library. Well, at the end of a little amateur performance.

    http://github.com/preprocessor/aasm

    A mechanism for storing states in the database as integers has been implemented. Performance and all that. Easy to use: Named scopes continue to work as they should. http://github.com/preprocessor/railroad_xing Fork fork (Ruby developers gentlemen, let's keep on githabe their projects at least in some form. The trend, after all). Adds support for aasm. As a result, we get:

    aasm_state :unread, :integer => 0
    aasm_state :read, :integer => 1
    aasm_state :closed, :integer => 2

    Conversation.aasm_integers[:read] => 1










     Why is this needed? With such a schema, it is often much easier to understand and discuss the code. However, its drawing will take 5-10 minutes. And if 10 models and often change? Naturally no one draws them. But if everything is automatic and convenient, then why not.

    Good luck.

    Upd. My fork railroad_xing is now mortal with the original. So you can follow and use github.com/royw/railroad_xing/tree/master


    Also popular now: