Boost logo

Boost :

From: Andreas Huber (ah2003_at_[hidden])
Date: 2004-06-01 16:03:34


Darryl Green wrote:
>> Right, but I don't think it is justified to complicate the interface
>> and the implementation (we'd need to add entry()/exit()) for that.
>> As I've explained in my answer to your other post you can always
>> move such a variables into an outer context that exists during a
>> transition.
>
> You seem to be missing my point. The very feature that is (afaik)
> unique to your fsm library is made far less useable by this design
> decision. The feature in question is the "state local storage".
> Moving it up a level is all very well but it doesn't allow RAII based
> on state because it must be in the ICOS or higher. I want the state
> local storage gone in the new state. I can either write a "pre-exit"
> action in a react() function,

Right. A very nice way to solve the problem, BTW.

> or run it in the exit (destructor),
> which is only useful for those actions that do not depend on the
> event. I would like to be able to define transitions in the state
> that will make them, and refer to that state's context in the action.
> I really don't see what is so bizarre about that.

It isn't bizare, it's only surprising because:
1. It allows that states are accessed when their exit() function has already
run. People putting their exit code into exit() might not expect that the
transition action might later again access the state. They could assume that
it's the other way round.
2. It makes entry/exit asymmetrical. A programmer changing a state having
only an entry action (i.e. a ctor) could very quickly come to the conclusion
that he needs to add a destructor for an exit action.
Both make the library a bit less intuitive to use. However, this is not the
biggest problem. I'm much more concerned about the fact that your proposal
breaks existing client code (currently, there are at least 2 independent
projects using boost::fsm) to achieve something that can already be done. In
FSM terms your "pre-exit" trick is an in-state reaction immediately followed
by a transition and I think it is much more intuitive to do it this way than
the way you propose. Plus, it doesn't even require more code.

Don't get me wrong, I'm not strictly against breaking existing client code,
it just doesn't seem justified in this case.

> Note that I'm quite
> happy to leave entry() == constructor (see below).
>
>> A state-specific recovery function would make error handling much
>> more difficult, as you have no idea what you need to do to bring the
>> state machine back into a stable state. If you e.g. make a
>> transition from such a state, it is not guaranteed that the machine
>> is stable afterwards (see Exception handling in the tutorial).
>
> You keep saying this. I have seen the exception handling in the
> tutorial.

Sorry, this has become a bit of a reflex lately.

> As far as I know your arguments re exit actions not failing are:
>
> 1) All exit actions must run. This is because every subsequent action
> (be it an outer exit action, the transition action, or an entry
> action in going to the new state) may reasonably depend on the
> successful execution of all prior exit actions.

Right.

> 2) Exit actions must run because their side effects :-) can be
> important - don't forget to put those rods back in the reactor....

Right.

> 3) Exit actions are often be logically paired with entry actions in a
> way similar to C++ ctor/dtor in order to implement RAII like idioms.
> The resource may be something physical like "the valve" and
> acquisition may mean "turn on" and release may mean "turn off".

Right.

> My view (fwiw) on these is:
>
> 1) This is very important. See below.
>
> 2) This is a usage decision best left to the user - any action, not
> just exit actions, can be critical - or not.

Exit actions are more critical than others in the current implementation
because the state machine can be terminated pre-maturely at any time
(state_machine<>::terminate()). UML clearly defines what termination means.
Now, you could rightly argue that state_machine<> should not have a
terminate() function as this can easily be implemented with a reaction in an
outermost state, which is triggered by a user-defined event. I'm not saying
this is a problem at all, I just want to show the implications of making
exit actions equal citizens.
One more thing that makes exit actions different is that an entry action
failure or transition action failure often leads to states being exited
before the problem can be handled.

> 3) I think this is a red herring

Do you mean a logical fallacy?

> - you represent state activation by
> constructing an object, so you have c'tors and d'tors as ideal places
> to put those "actions" that do map precisely to these concepts,
> regardless of whether you also provide exit actions. Trying to stick
> rigidly to the UML spec

I think this *is* quite important. I believe not sticking to UML would lower
the acceptance of the library a lot.

> and use a rigidly defined language mechanism
> to implement it is very inflexible.

I agree that using destructors makes it slightly less flexible.

> Note I haven't mentioned anything about c'tor/entry action mapping.
> This is because I don't think there is any real distinction between
> entry actions and c'tors. There are plenty of languages (java etc)
> with c'tors to perform initialisation, which is similar to the
> purpose of an entry action, but that don't provide object
> destruction/destructors (at least not in a RAII compatible way), so I
> don't see anything evil about the use of one and not the other.

In such languages RAII is achieved with a separate Dispose() function. If I
was to port the library I'd have users provide Dispose() to implement an
exit action.

> I propose the following handling of failing exit actions, which as
> far as I can see addresses the important item (1) above.
>
> Use a mechanism essentially the same as what you have now for failure
> handling.
>
> If exit() of a state fails the innermost outer state is checked for a
> handler for the failure. If there is one, the transition is made. If
> there isn't, it is an irrecoverable failure (can't run any more exit
> actions). At this point the fsm is simply destructed (without running
> exit actions) and the exception rethrown.

Ok so far, this is certainly doable.

> Restrict the allowable transition to be to an inner state only (once
> again, avoids any further exit actions).

I don't understand this. You said that the reaction should be searched in
the ICOS. If you make a transition from the ICOS to a state that is an inner
state of the one whose exit action failed then the ICOS and all its inner
states must be left (again this behavior is required by UML), right?

> When writing a reaction for exit handle failure, be aware that as an
> inner exit handler has failed, certain preconditions which would
> exist for other transitions, don't. This is not any different to any
> other action failing afaiks.

I think it *is* genuinely different. A failing exit action prevents the
calling of the exit actions of outer states. Once an exit action has failed
you can never leave the state again, *unless* you find a way to somehow
recover and then retry exiting the state. If you cannot handle the problem,
the only thing you can do is to enter inner states or abort everything and
rethrow the exception.

A failure of the other actions never brings you in a situation where you are
*forced* to either retry the previously failed action or bail out
completely.

> I would envisage using such a mechanism by making the transition to a
> recovery state used only for that purpose. The recovery state, as a
> sibling of the failed state has access to all the context the failed
> state had (except for the now destructed failed states own).

We have never left the state whose exit action has thrown, right? How can we
then enter a sibling (in StopWatch e.g. Running and Stopped are siblings)?

>>> My specific concern is the use of the extended functionality to
>>> produce states which have/build significant context that (should)
>>> only exist until a transition action deals with it. I don't see that
>>> using this state context after the exit action has run is a problem.
>>> The exit action may well have added to or modified it to make the
>>> context complete, not damaged (and in particular not destructed) it.
>>
>> That's true, see above.
>
> See what above? That I could move the context out a level? See above
> :-) for why this doesn't address the problem as I see it.

Hmm, I don't remember why I wrote "see above". I guess it wasn't that
important. Anyway, you are right with this observation. However as I've
explained above ;-), I'd rather have this handled with your pre-exit trick.

[example snipped]

Ok, I see (finally ;-)) that there might be a use case for exit(), namely
the one that you don't want to run certain actions on destruction, but you
do want to run them on termination. exit() also doesn't break any existing
code, as destructors keep acting as they do now.

We have to define more thoroughly what happens when exit() throws.

Regards,

Andreas


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk