Suppose I have a type Thing
with a state property A | B | C
,
and legal state transitions are A->B, A->C, C->A
.
I could write:
transitionToA :: Thing -> Maybe Thing
which would return Nothing
if Thing
was in a state which cannot transition to A
.
But I'd like to define my type, and the transition functions in such a way that transitions can only be called on appropriate types.
An option is to create separate types AThing BThing CThing
but that doesn't seem maintainable in complex cases.
Another approach is to encode each state as it's own type:
data A = A Thing
data B = B Thing
data C = C Thing
and
transitionCToA :: C Thing -> A Thing
This seems cleaner to me. But it occurred to me that A,B,C are then functors where all of Things functions could be mapped except the transition functions.
With typeclasses I could create somthing like:
class ToA t where
toA :: t -> A Thing
Which seems cleaner still.
Are there other preferred approaches that would work in Haskell and PureScript?
You could use a type class (available in PureScript) along with phantom types as John suggested, but using the type class as a final encoding of the type of paths:
Now you can create a type of valid paths:
Paths can only be constructed by using the one-step paths you provide.
You can write instances to use your paths, but importantly, any instance has to respect the valid transitions at the type level.
For example, here is an interpretation which calculates lengths of paths:
If you want to store a list of transitions so that you can process it later, you can do something like this:
You can turn that into an instance of
Path
by writing a concatenation function forDomino
:In fact, this makes me wonder if Hackage already has a package that defines "indexed monoids":
Here's a fairly simple way that uses a (potentially phantom) type parameter to track which state a
Thing
is in:Since your goal is to prevent developers from performing illegal transitions, you may want to look into phantom types. Phantom types allow you to model type-safe transitions without leveraging more advanced features of the type system; as such they are portable to many languages.
Here's a PureScript encoding of your above problem:
Phantom types work well to model valid state transitions when you have the property that two different states cannot transition to the same state (unless all states can transition to that state). You can workaround this limitation by using type classes (
class CanTransitionToA a where trans :: Thing a -> Thing A
), but at this point, you should investigate other approaches.