Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Label Question. How to you add fields to the state and side-effects #1

Open
ndrean opened this issue Feb 4, 2023 · 4 comments
Open
Labels
enhancement New feature or request

Comments

@ndrean
Copy link

ndrean commented Feb 4, 2023

Thanks for this module. I take a simple example. I want to add a counter in the state and run a side effect such as incrementing it at to specific transition in a synchronous manner.
I can add via :external my counter and see that the `context.instance" . I can indeed change the value with an "external" action, however, no action onto this can be made from within the %X.Machine{} struct, or I don't see how. I can only launch "external" functions. It seems you run these functions async. Some side-effects should be done concurrently but in my case of an internal state, it shouldn't I believe.

The following does not work:

use Xstate.StateMachine
alias Xstate.StateMachine, as:  X

machine =
  %X.Machine{
    initial_state: "unlocked",
    mapping: %{
      :lock => %X.Transitions{
        target: "locked",
        before: nil,
        callback: fn  context ->
         %{context | internal: %{count: context.instance.count +1}}
      end
     },
    :unlock => %X.Transitions{
        target: "unlocked",
        before: nil,
        callback: nil
     }
  },
  modifiable_states: MapSet.new(["open"])
}

# init
machine =  X.new(machine, %{count: 0})
machine.external.count == 0
#true ok

# "internal change doesn't work"
%X.Transition(machine, "close")
%X.Transition(machine, "open")
machine.external.count == 0 
#true

# "external" update ok
machine = %{machine | external: %{count: machine.external.count + 1}}
machine.external.count == 1
%X.Transition(machine, "close")

#true
@natserract
Copy link
Owner

Thanks for the questions,

  1. First, when you call transition
%X.Transition(machine, "close") # close??
%X.Transition(machine, "open") # open??

Events close, open are not on the machine, it seems only lock and unlock. Therefore the state does not change. Maybe you can take a look api reference https://hexdocs.pm/xstate/Xstate.StateMachine.html, and see several tests xstate tests

  1. Notes, context is just the return of a value after transition, so it can't be changed.
 Parameters:

    pid : PID (process identifier)
    event: The event that caused the transition
    access_time: Access time
    state: The resolved machine state, after transition
    instance: Any external value you want to pass it, passed from: external
  1. Regarding side effects, it seems you want to run side effects simultaneously when the transition starts, for example transitionA, transitionB, when transitionA starts, run side effect A + sideEffect B, you can use the parameters :before or :callback in each of the transitions.

And true, it's async because:

The problem of being "between states" during a state transition is circumvented by imposing the run-to-completion (RTC) semantics of processing events, which all state machine formalisms (including UML statecharts) universally assume. RTC means that a state machine must always complete processing of the previous event before it can start processing the next.

@ndrean
Copy link
Author

ndrean commented Feb 5, 2023

Thanks for your response and for clarifying the RTC concept.

For 1., I made a mistake indeed, sorry for that. it is of course as below.

X.Transition(machine, ":lock")
%X.Transition(machine, "unlock")

The transition job works perfectly. In fact, I wanted to be able to access this :external from the callback for a given state in the X.Machine. That's why I looked into context. I just experimented with a simple map %{coount: i} and couldn't reach it. My idea was to let X.Machine do a job on the database so I wanted to access it with X.Machine. I hope this clarifies my intentions.

@natserract
Copy link
Owner

That's good point! I think for now I haven't developed further about :external value. So far only as normal dependencies, maybe you can use return value of this context ex. instance depends on other values ​​(outside the machine) or can use 2 machines.

@natserract natserract added the enhancement New feature or request label Feb 5, 2023
@ndrean
Copy link
Author

ndrean commented Feb 5, 2023

You shed light on the RFC. Your approach ressembles to Amazon Step Functions.

If you allow me to share some ideas...The data added to the machine state through they key :external can only be modified from outside a machine, thus are run in sequence outisde of the machine so we are assured that the next transition will have an up-to-date machine state. But in the same way you can callback: fn context -> IO.inspect(context), I was thinking of accepting before/callback: fn external -> atomic_modification(%{count: c} = external) |> await, so it is a transaction, "atomic" and RFC is assured with the state machine up-to-date. If my :external was an Ecto.changset, then I would use an Ecto.Mutli.run which can run arbitrary code inside a transaction. This is opposed to a pur "side-effect" callback, such as "send-an-email": just running a task to run a task to forget about.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants