StoneCypher / fsl

Finite State Language specification
9 stars 1 forks source link

Hooks #255

Open StoneCypher opened 4 years ago

StoneCypher commented 4 years ago

This is the core issue for hooks as a concept

Hooks tell the outside after something has happened. They offer the ability to reject transitions, modify the internal data state, consume from the input tape, push onto the output tape, throw errors, halt the machine, or cause transitions. All of these things can be initiated from outside code.

Cross reference

Hook lifecycle is defined in #487. Notation is found in Hook notation #622, and short notation in #619. Transactionality is in #452. Required hooks are described in #620. Fluency is discussed in #596. Hook closure is in #617. The external hooking API is described in #700; the basic hooking API is described in #660, and the fluent in #699. Posthooks are covered in #896. Hook next is described in #952.

Specific hooks needed

The Basic hooks are almost done. The future hooks are mostly not.

todo: get the terminate variants and add them here

todo: hooks for data change

StoneCypher commented 4 years ago

@machinshin

StoneCypher commented 3 years ago

Remember, we lost a user over this

Get this done

https://github.com/StoneCypher/jssm/issues/389

theshadow27 commented 3 years ago

I was also very excited about the jssm project - especially the DSL and graphing - until I realized there were no callback functions/eventEmitter support. Graphical view for system validation is extremely nice, but without events there is so much glue that it looses it's advantage over something like Finity with no DSL and difficult to read source but native event support.

I think the hardest part is going to be incorporating into the DSL... though looking through samples, I think that the extreme test machine the ${functionName} syntax is pretty elegant.

An alternate approach would be to forgo DSL entirely and just populate the Machine object with functions per the state name. This has the advantage of boilerplate reduction, convention over configuration, and keeping the DSL tight. For example, the Javascript-State-Machine project just invokes functions if present, and adds accessors (this can probably be done with a proxy, or by actually adding them during parse).

.. methods to transition to a different state: fsm.melt() fsm.freeze() fsm.vaporize() fsm.condense() ... observer methods called automatically during the lifecycle of a transition: onMelt() onFreeze() onVaporize() onCondense()

presumably you'd also want beforeMelt(), onMelt(), afterMelt() .. .

If Machine inherits EventEmitter (which is pretty cheap) then the default 'onState' functions can invoke this.emit('State') etc.

If all of this seems too expensive for cases where it is unused, then a small DSL extension like

events: on;

--- or ---

events: [freeze melt]
before-events: [condense]

would let users opt in... or a shorthand (RegExp-ish /e for instance) to "just turn it all on" to maintain the beautiful (mega props, BTW) one liners:

const TrafficLightsEventEmitter = sm`Red -> Green -> Yellow -> Red; /e`;

Just some thoughts. On a tight deadline so I will have to use something else, but I hope to use (~> contrib?) to jssm in the future. Great work thus far!!

PS - regarding data-pass path, my (totally arbitrary and entirely personal) preference would be to follow the DOM-ish model where the callback would get the source (the Machine, and any properties set on it) followed by any args passed to the trigger.

const TrafficLight = sm`Red -> Green -> Yellow -> Red; /e`;
TrafficLight.on('before-Green', function(  fsm, data ){ ... assert fsm === TrafficLight } );

// all the same:
TrafficLight.transition('Green', data)  ;
TrafficLight.Green(data);
TrafficLight.emit('transition', 'Green', data);

(edit: that was weird autocorrect... )

StoneCypher commented 3 years ago

that's actually almost exactly the planned api (enter rather than emit because of ISO naming)

the reason i've been dragging my heels is that this is going to be cross-language, and defining that in a cross language way is brutal

i'm thinking about opting out tbh and mumbling some "the implementation of which is left to the reader" type nonsense

theshadow27 commented 3 years ago

yeah I saw your cross-language tickets... very ambitious... but at least in node (jssm) it is a very tough sell w/o any support.

The EventEmitter->jssm is low priority, one line of code could bind all actions...

const fsm = sm`Red -> Green -> Yellow -> Red; `;
const bus = new EventEmitter(fsm);

fsm.list_actions().forEach( a => bus.on(a, fsm.action.bind(fsm, a) ) );

but a correct jssm->EventEmitter is not possible without completely re-implementing the action/transition/force_transition functions.

would you be open to at least adding a couple empty functions on Machine, like

guard(what: string, action: StateType, lastState: StateType, nextState: StateType, data?: mDT): boolean {
   return true; // TODO implement events+guards
}
mutating(direction: string, state: StateType,  data?: mDT): void {}

and stubbing out

  action(name: StateType, newData?: mDT): boolean {

    if (this.valid_action(name, newData)) {
      const edge: JssmTransition<mDT> = this.current_action_edge_for(name);

      if(!this.guard( 'action' , name, this._state, edge.to, newData ) )  return false; /* NEW! */

      this.mutating('exit', this._state, newData);
      this._state = edge.to;
      this.mutating('enter', this._state, newData);

      return true;
    } else {
      this.guard('invalid_action', name, this._state, null, newData);  /* NEW just a callback */
      return false;
    }
  }

  transition(newState: StateType, newData?: mDT): boolean {
    if (this.valid_transition(newState, newData)) {

      if(!this.guard( 'transition' , 'transition', this._state, edge.to, newData ) )  return false; /* NEW! */

      this.mutating('exit', this._state, newData);   /* NEW! */
      this._state = newState;
      this.mutating('enter', this._state, newData);     /* NEW! */

      return true;
    } else {
      this.guard( 'invalid_transition' , 'transition', this._state, newState, newData );  /* NEW! just a callback */
      return false;
    }
  }

// same for forced_transition

stays synchronous/deterministic, no changes to the DSL, only two new tests to keep 100% coverage (again, major props there), and the pattern is portable across any language with inheritance/prototyping. In ES, the JIT will immediately prune the branches and empty functions -- I'd bet $1 there would be no affect on performance. And then, for those readers so inclined, we could have decent event support with three nominally-one-liners:

const fsm = sm`Red -> Green -> Yellow -> Red; `;
const bus = new EventEmitter(bus);

fsm.list_actions().forEach( a => bus.on(a, fsm.action.bind(fsm, a) ) );
fsm.mutating=(direction, state,  data)=>{  bus.emit(direction, state, data );  if(direction==='enter') bus.emit(state, data); }
fsm.guard = (what, action, lastState, nextState, data) => { 
   try { 
       bus.emit( what.startsWith('invalid_') ? what :  `>${action}` , {action, data, lastState, nextState}) ;
       return true;
   } catch(e){return false;}
};

or something like that... anyway, just my $0.02. I'd offer a PR but I'm not a TypeScript guy and the code is too pretty for me to butcher.

p.s. I ended up going with Finity not just for events but also because of the async support and builtin timeouts, which the above doesn't cover (though it is also easy to shim). Have to say after playing with jssm it is EXTREMELY annoying not being able to paste the code into a web-based viewer to verify the diagram. So quit dragging your feet already ;P but really, perfect is the enemy of good enough.

p.p.s. per the checklist, this covers :

✅ Entering: mutating('enter', ... ) ✅ Exiting: mutating('exit' , ...) ❌ Specific transition: in transition there is no edge lookup, though there could be (at additional cost?)... least in UML-ish state diagrams, mutating('enter' | 'exit' , ...) covers everything (not immediately obvious to me if duplicate edges are allowed in jssm) ❌ Standard/Main/Forced transition: Nice to have for api symmetry but unclear as to the value in practice (use case?). Possible with edge lookup per above ❓ Data change: ... as data not stored currently, out of scope... ✅ Specific action: emit(action) in guard() ✅ Any transition/action: emit('*') in every non-invalid call to guard() (transition is just an unnamed action) ✅ Starting: mutating('start', start_states[0], this) in constructor ✅ Termination: state in this.end_states in the mutating override

edit(again): sorry, It's late, should have been fsm.action.bind(fsm, a)

StoneCypher commented 3 years ago

Got asked again by another new user in email today

Really need to get this done

StoneCypher commented 3 years ago

the code is too pretty for me to butcher.

i just saw this

😂😂😂😂😂😂😂😂

my code is, in reality, a trash fire

theshadow27 commented 3 years ago

Eye of the beholder 😉

But really, the simplicity is very nice.

Looking forward to see what you come up with. No pressure hah

On Thu, May 6, 2021 at 18:30 John Haugeland @.***> wrote:

the code is too pretty for me to butcher.

i just saw this

😂😂😂😂😂😂😂😂

my code is, in reality, a trash fire

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/StoneCypher/fsl/issues/255#issuecomment-833915611, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABJ5Y4WJVWCKGHXNZLWHPKTTMMKBLANCNFSM4J454AWA .

StoneCypher commented 2 years ago

@theshadow27 - hooks on fully named transitions and on actions have landed

const tl = sm`Red => Green => Yellow => Red;`
  .hook('Yellow', 'Red', () => console.log('Red light!');
StoneCypher commented 2 years ago

and for the record i ended up implementing almost everything you suggested, in ways quite similar to what you suggested

your recommendations were solid