10

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?

Mark Bolusmjak
  • 23,606
  • 10
  • 74
  • 129
  • Do you want the edges to *be* or to *do*? I'm asking because @Daniel Wagner presents a solution in his answer where edges are functions, and I'm curious if you're interested in a solution where edges are pure data (so you can give a sequence of state transitions several semantics after-the-fact) – Cactus Aug 18 '15 at 06:58
  • @Cactus my goal is making it clear to developers what transitions are legal through the type system. I want to avoid an API where calling an illegal transition is possible, and returns a `Nothing` or results in some failure. I'm interested in what you propose if it's possible with those constraints. – Mark Bolusmjak Aug 18 '15 at 13:54

4 Answers4

10

Here's a fairly simple way that uses a (potentially phantom) type parameter to track which state a Thing is in:

{-# LANGUAGE DataKinds, KindSignatures #-}
-- note: not exporting the constructors of Thing
module Thing (Thing, transAB, transAC, transCA) where

data State = A | B | C
data Thing (s :: State) = {- elided; can even be a data family instead -}

transAB :: Thing A -> Thing B
transAC :: Thing A -> Thing C
transCA :: Thing C -> Thing A

transAB = {- elided -}
transAC = {- elided -}
transCA = {- elided -}
Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380
  • 1
    Perhaps a bit nicer with a type class: `class Trans x y where trans :: Thing x -> Thing y`. – effectfully Aug 17 '15 at 23:09
  • @user3237465 not as handy as you may think. What is `thing . thing` for example? – phadej Aug 18 '15 at 15:25
  • @phadej, it's not possible to even state this with `trans**` functions, so you don't lose anything. If you want instantiated versions of `trans`, you can always add them, or, if rules are hardcoded, you can make a `GADT`, indexed by two `State`s, and define `transBy` (and you can go further with `Constraints`). `trans . trans` is [`transThrough`](http://ideone.com/xA8Fnn). – effectfully Aug 18 '15 at 16:38
  • 1
    for `Staty` use of `Data.Proxy` is enough. IMHO this is an example where you shouldn't do stuff, even you can. OTOH, `class Trans x y` enforces, that transition from some `x` to `y` is unique, if exists. – phadej Aug 18 '15 at 16:55
  • Which "other answer"? – dfeuer Aug 19 '15 at 02:20
  • @dfeuer It seems the answer I was referring to has been deleted for some reason. I've updated my answer to be self-contained. – Daniel Wagner Aug 19 '15 at 04:00
  • @phadej, "for `Staty` use of `Data.Proxy` is enough" — good point. Haskell made me more hasochistic than it's needed. @dfeuer, mine, but I deleted it, since it was misleading. – effectfully Aug 19 '15 at 04:02
5

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:

data A -- States at the type level
data B
data C

class Path p where
  ab :: p A B -- One-step paths
  ac :: p A C
  ca :: p C A
  trans :: forall a b c. p c b -> p b a -> p c a -- Joining paths
  refl :: forall a. p a a

Now you can create a type of valid paths:

type ValidPath a b = forall p. (Path p) => p a b

roundTrip :: ValidPath A A
roundTrip = trans ca ac

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:

newtype Length = Length Int

instance pathLength :: Path Length where
  ab = Length 1
  ac = Length 1
  ca = Length 1
  trans (Length n) (Length m) = Length (n + m)
  refl = Length 0
Phil Freeman
  • 4,199
  • 1
  • 20
  • 15
2

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:

foreign import data A :: *
foreign import data B :: *
foreign import data C :: *

data Thing a = Thing

transitionToA :: Thing C -> Thing A

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.

John A. De Goes
  • 568
  • 3
  • 7
2

If you want to store a list of transitions so that you can process it later, you can do something like this:

{-# LANGUAGE DataKinds, GADTs, KindSignatures, PolyKinds #-}

data State = A | B | C
data Edge (a :: State) (b :: State) where
    EdgeAB :: Edge A B
    EdgeAC :: Edge A C
    EdgeCA :: Edge C A

data Domino (f :: k -> k -> *) (a :: k) (b :: k)  where
    I :: Domino f a a
    (:>>:) :: f a b -> Domino f b c -> Domino f a c

infixr :>>:

example :: Domino Edge A B
example = EdgeAC :>>: EdgeCA :>>: EdgeAB :>>: I

You can turn that into an instance of Path by writing a concatenation function for Domino:

{-# LANGUAGE FlexibleInstances #-}
instance Path (Domino Edge) where
    ab = EdgeAB :>>: I
    ac = EdgeAC :>>: I
    ca = EdgeCA :>>: I

    refl = I
    trans I es' = es'
    trans (e :>>: es) es' = e :>>: (es `trans` es')

In fact, this makes me wonder if Hackage already has a package that defines "indexed monoids":

class IMonoid (m :: k -> k -> *) where
    imempty :: m a a
    imappend :: m a b -> m b c -> m a c

instance IMonoid (Domino e) where
    imempty = I
    imappend I es' = es'
    imappend (e :>>: es) es' = e :>>: (es `imappend` es')
Community
  • 1
  • 1
Cactus
  • 27,075
  • 9
  • 69
  • 149
  • 1
    `Domino Edge` should, I believe, be an instance of Phil Freeman's class. This structure seems to suggest that the class be broken up into one describing single edges, one enforcing reflexivity (allowing do-nothing transitions), and one enforcing transitivity (allowing arbitrary chaining). Relaxing either of the latter, or imposing other rules like symmetry, changes the allowable systems. – dfeuer Aug 19 '15 at 02:47
  • @dfeuer: I've added a `Path` instance, and realized in the process that what we have is an indexed monoid. – Cactus Aug 19 '15 at 03:36
  • 1
    `Domino` is precisely [`Star`](https://github.com/agda/agda-stdlib/blob/master/src/Data/Star.agda). – effectfully Aug 19 '15 at 03:44
  • @user3237465: Yes! Dammit I was sure I've seen it somewhere before. – Cactus Aug 19 '15 at 03:47
  • 1
    Ha! I didn't mention that because I was sure you chose the name in jest. – dfeuer Aug 19 '15 at 09:24
  • 1
    I think `Domino` is a nicely evocative name for this. – Cactus Aug 19 '15 at 09:26
  • 1
    Your IMonoid is equivalent to [Category](http://hackage.haskell.org/package/base-4.9.1.0/docs/Control-Category.html). – Andrew Thaddeus Martin Feb 17 '17 at 13:38