Boost logo

Boost :

From: Darryl Green (darryl.green_at_[hidden])
Date: 2004-06-02 01:10:23


Andreas Huber <ah2003 <at> gmx.net> writes:

>
> 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.

I have been trying to formulate a reasonable model that is a pure extension to
UML statechart semantics to deal with this - I think I have. It also shouldn't
break any code that currently uses boost:fsm. My proposal is to allow a
transition action's context to be *any* outer state of the source state and to
change the state transition processing to:

1) Exit and destruct out to (but not including) the transition action's
context or the ICOS, whichever comes first.

2) Execute transition action.

3) Exit and destruct any remaining states out to the ICOS.

4) Enter destination states.

This doesn't allow access to the state being exited in the transition action,
but it does allow the action of a transition that exits an outer state to
access that state's context "on the way out". I am finding this to be more
frequent/useful than access to an individual simple state's context anyway.
Where the requirement is really to access an individual state the cost is a
split into an inner and outer state.

There is afaiks no impact on exception handling (I don't think it changes the
docs, but it my change the implementation - I haven't checked). If the
transition action fails, the exit actions up to the ICOS should run, then the
failure reaction is searched for from ICOS out.

Note that it doesn't seem to be hard to make this facility optional - a
compiletime option could determine whether a context inside of the ICOS was a
legal action context parameter or not.

> 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.

True.

> 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.

Intuitive to you ;-) I don't think it is too bad in simple cases, but it gets
ugly when what I really wanted was more like what is described in my proposal
above (access to an outer state's context), which I've found my usage evolving
into, more often than not.

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

Ok - I also want to avoid breaking existing code.

> > 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.

I agree with this. I overstated my case somewhat. I think you will see in my
later description of exception handling that I do conceed that exit is
critical in some ways. I'm having trouble clearly delineating the features of
exit that need special handling, and felt that this case was one that didn't.
I think this is just a very slight difference of opinion on how to decompose
the features of exit, and not of any great importance overall.

> 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.

Ultimately, I don't make them equal.

> > 3) I think this is a red herring
>
> Do you mean a logical fallacy?

I think it is in the sense that you can't infer that because some aspect of A
is the same as a corresponding aspect of B that A is B.

>
> > - 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.

I didn't mean to say that the library shouldn't stick to the UML spec where it
is applicable. I don't think that it is applicable in the area of object
lifetime management because in UML states are not objects. In any case, UML
statecharts (or Harel statecharts) aren't the only/ultimate/most rigourous
definitions of FSM semantics. It isn't hard to find a plethora of possibly
more rigorous FSM models (I'm not making any claims for them, I have just seen
a lot of citations - not read all the papers). One thing that is not so easy
to find is anything regarding integrating FSM as an aspect of a multi-paradigm
programming language like C++ (yuk - reads like an add - lucky I didn't
say "rich API" as well...). I think your work is very valuable here, and that
dealing with object lifetime issues is an important part of that. However, it
is relatively virgin teritory, afaik. It would be remiss of me to fail to
invest at least some effort in contesting some of your claims for this new
approach. Apprently a number of other people feel the same way ;-)

>
> > 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.

Precisely. I don't mean that it can't be done, only that the C++ view that
what a ctor does a dtor "undoes" doesn't always hold in other contexts, why in
this one? I suggest that users have to provide an exit() to implement an exit
action. If you think it will help Java programmers, I guess you could call it
dispose() -)

>
> > 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.

No I didn't I said innermost outer state, (IOS) not ICOS. A better term might
have been immediate outer state (of the state that had exit fail). I'm going
to write IOS from now on, and hope that the definition is clear enough.

> 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?

Yes. That is why I propose that the reaction must be in the IOS, as that
avoids exiting any more states before making the transition.

> > 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.

As does my proposal.

> 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.

I reached my proposed solution after discounting the approach of entering
inner states to do recovery, because it won't work for an innermost state with
an exit action that can fail (because the recovery state would then be the
innermost state, making the original state unstable). I guess some
ephemeral "recovery state" concept could deal with this and only
construct/enter an inner recovery state when needed, otherwise treating the
recovery state's IOS as the innermost state. I haven't checked to see how
boost::fsm behaves if you don't specify the initial inner state (illegal in
UML afaik).

> > 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?

That was my first preference, but it created the problem of ephemeral inner
recovery states. So instead, I proposed considering exit to be "done"
but "failed". This seems reasonable enough - analogous to exception handling
where you aren't in the block that threw any more, you are in the catch block.
This means that after the exit action runs and fails, the state's destructor
must run (and succeed - don't throw in the dtor or we are doomed obviously).

> How can we then enter a sibling (in StopWatch e.g. Running
> and Stopped are siblings)?

I think the above explained it. If we were in Running, and its exit action
failed, this would have to be reacted to by Active, which could then make a
transition to stopped (or more likely, a 3rd sibling that dealt with recovery
from failing to exit Running).

> 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.

Yep.

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

I hope I just did.

Regards
Darryl.


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