Skip to content

Instantly share code, notes, and snippets.

@jannesiera
Created May 3, 2020 18:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jannesiera/260a4891ef61baf8ac3b8d0c51443c53 to your computer and use it in GitHub Desktop.
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 ()
@MargaretKrutikova
Copy link

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? And type ValidS = private | ValidS? Why do you need private here?

@jannesiera
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment