-
-
Save jannesiera/260a4891ef61baf8ac3b8d0c51443c53 to your computer and use it in GitHub Desktop.
module rec StateMachine = | |
/// No safe transitions /// | |
type ButtonState = | |
| Invalid // Show red and disabled button | |
| Valid // Show green and enabled button | |
| Loading // Show a greyed out disabled button, with a loading animation | |
type Action = | |
| Validate // When form is valid | |
| Invalidate // When form is invalid | |
| Click // When a valid button is clicked | |
| Reset // When data is loaded | |
let transition state action = | |
match state, action with | |
| Invalid, Validate -> Valid | |
| Valid, Invalidate -> Invalid | |
| Valid, Click -> Loading | |
| Loading, Reset -> Invalid | |
| _ -> failwith "Invalid state transition detected" | |
/// Encoding 1 /// | |
type InvalidTransition = | |
| Validate | |
type ValidTransition = | |
| Invalidate | |
| Click | |
type LoadingTransition = | |
| Reset | |
type ButtonState = | |
private | |
| Invalid of (InvalidTransition -> ButtonState) // Show red and disabled button | |
| Valid of (ValidTransition -> ButtonState) // Show green and enabled button | |
| Loading of (LoadingTransition -> ButtonState) // Show a greyed out disabled button, with a loading animation | |
let (|InvalidS|ValidS|LoadingS|) = function | |
| ButtonState.Invalid t -> InvalidS t | |
| ButtonState.Valid t -> ValidS t | |
| ButtonState.Loading t -> LoadingS t | |
let private Invalid = | |
ButtonState.Invalid (function | |
| Validate -> Valid | |
) | |
let private Valid = | |
ButtonState.Valid (function | |
| Invalidate -> Invalid | |
| Click -> Loading | |
) | |
let private Loading = | |
ButtonState.Loading (function | |
| Reset -> Invalid | |
) | |
let init = Invalid | |
module Consumer = | |
open StateMachine | |
let next = | |
match init with | |
| InvalidS t -> t Validate | |
| ValidS t -> t Invalidate | |
| LoadingS t -> t Reset | |
let next2 = | |
match next with | |
| InvalidS t -> t Validate | |
| ValidS t -> t Invalidate | |
| LoadingS t -> t Reset | |
/// Encoding 2 (phantom types) /// | |
module ButtonStateMachine = | |
type ValidS = private | ValidS | |
type InvalidS = private | InvalidS | |
type LoadingS = private | LoadingS | |
type ButtonState = | |
| Valid of ValidS | |
| Invalid of InvalidS | |
| Loading of LoadingS | |
let Validate (pt: InvalidS) = Valid ValidS | |
let Invalidate (pt: ValidS) = Invalid InvalidS | |
let Click (pt: ValidS) = Loading LoadingS | |
let Reset (pt: LoadingS) = Invalid InvalidS | |
let init = Invalid InvalidS | |
module Consumer = | |
open ButtonStateMachine | |
let next = | |
match init with | |
| Invalid s -> Validate s | |
| Valid s -> Invalidate s | |
| Loading s -> Reset s | |
/// Encoding 3 /// | |
module rec ButtonStateMachine = | |
type ButtonState = | |
private | |
| Invalid of {| Validate : unit -> ButtonState |} | |
| Valid of {| Invalidate : unit -> ButtonState; Click : unit -> ButtonState |} | |
| Loading of {| Reset : unit -> ButtonState |} | |
let (|Invalid|Valid|Loading|) = function | |
| ButtonState.Invalid t -> Invalid t | |
| ButtonState.Valid t -> Valid t | |
| ButtonState.Loading t -> Loading t | |
let private CreateInvalid () = Invalid {| Validate = CreateValid |} | |
let private CreateValid () = Valid {| Invalidate = CreateInvalid; Click = CreateLoading |} | |
let private CreateLoading () = Loading {| Reset = CreateInvalid |} | |
let init = CreateInvalid () | |
module Consumer = | |
open ButtonStateMachine | |
let next = | |
match init with | |
| Invalid s -> s.Validate () | |
| Valid s -> s.Invalidate () | |
| Loading s -> s.Reset () | |
Yes, the idea is that when you are in a certain state you only have access to the transitions that are valid in that state. I imagine this could be used when e.g. write a React component or hook. When you look at other solutions in the React ecosystem, people write state machines in a way where you can launch any transition in any state, even when these transitions are not valid in the current state. With this solution you only have access to the transitions within the pattern match, which will only provide you with valid transitions.
Most of the syntactic features here are to ensure that someone can't just create a new state out of thin air, because then they could circumvent the transitions. If you feel like you can be disciplined about this, they might not be strictly necessary for you and it would simplify the code somewhat. Having them there prevents you from using the API in an incorrect way though.
let (|InvalidS|ValidS|LoadingS|) = function
is called an 'Active Pattern' in F#. It is a way to be able to pattern match on the discriminated union. Because the actual DU is marked private, you are unable to create new instances with the data constructors: that's good, you should only be allowed to create a state by following a transition. But this also has the side effect that, outside of the module, the DU is not available for pattern matching. That's why we need the active pattern to get around this. There is an open issue on this on the F# github repo to make it easier to do this in the language itself.
If you want to simplify the code for yourself, feel free to remove the 'private' on the DU and leave out the active pattern.
type ValidS = private | ValidS
needs private because otherwise you could manually create a new ValidS
outside of the module.
So the idea here is to eliminate unsafe transitions on the type level, right? Not sure I understand all the syntactic features used here.
What does this do
let (|InvalidS|ValidS|LoadingS|) = function
? Andtype ValidS = private | ValidS
? Why do you needprivate
here?