8

I am trying to replicate in Haskell this piece of Idris code, which enforces correct sequencing of actions through types:

 data DoorState = DoorClosed | DoorOpen
 data DoorCmd : Type ->
                DoorState ->
 DoorState ->
                Type where
      Open : DoorCmd     () DoorClosed DoorOpen
      Close : DoorCmd    () DoorOpen   DoorClosed
      RingBell : DoorCmd () DoorClosed DoorClosed
      Pure : ty -> DoorCmd ty state state
      (>>=) : DoorCmd a state1 state2 ->
              (a -> DoorCmd b state2 state3) ->
              DoorCmd b state1 state3

Thanks to overloading of (>>=) operator, one can write monadic code like:

do Ring 
   Open 
   Close

but compiler rejects incorrect transitions like:

do Ring
   Open 
   Ring
   Open

I have tried to follow this pattern in the following Haskell fragment:

 data DoorState = Closed | Opened

 data DoorCommand (begin :: DoorState) (end :: DoorState) a where
   Open  :: DoorCommand 'Closed 'Opened ()
   Close :: DoorCommand 'Opened 'Closed ()
   Ring  :: DoorCommand 'Closed 'Closed ()

   Pure  :: x -> DoorCommand b e x
   Bind  :: DoorCommand b e x -> (x -> DoorCommand e f y) -> DoorCommand b f y

 instance Functor (DoorCommand b e) where
   f `fmap` c = Bind c (\ x -> Pure (f x))

 -- instance Applicative (DoorCommand b e) where
 --    pure    = Pure
 --    f <*> x = Bind f (\ f' -> Bind x (\ x' -> Pure (f' x')))

 -- instance Monad (DoorCommand b e) where
 --   return = Pure
 --   (>>=) = Bind

But of course this fails: The Applicative and Monad instances cannot be defined correctly as they require two different instances to properly sequence operations. The constructor Bind can be used to enforce proper sequencing but I cannot manage to use the "nicer" do-notation.

How could I write this code to be able to use do-notation, e.g. to prevent invalid sequences of Commands ?

insitu
  • 4,488
  • 3
  • 25
  • 42
  • 3
    Though it is a trick I have merely heard about, I suspect you might be looking for [indexed monads](http://stackoverflow.com/q/28690448/2751851). – duplode Oct 22 '16 at 21:47
  • 3
    If you want, you can always also use `RebindableSyntax` to get `do` without having to make an actual `Monad`. – Alec Oct 22 '16 at 23:18

1 Answers1

9

What you're looking for is indeed Atkey's parameterised monad, now more commonly known as the indexed monad.

class IFunctor f where
    imap :: (a -> b) -> f i j a -> f i j b
class IFunctor m => IMonad m where
    ireturn :: a -> m i i a
    (>>>=) :: m i j a -> (a -> m j k b) -> m i k b

IMonad is the class of monad-like things m :: k -> k -> * -> * describing paths through a directed graph of types belonging to the kind k. >>>= binds a computation which takes the type-level state from i to j into a computation which takes it from j to k, returning a bigger computation from i to k. ireturn allows you to lift a pure value into a monadic computation which doesn't change the type-level state.

I'm going to use the indexed free monad to capture the structure of this sort of request-response action, largely because I don't want to have to figure out how to write the IMonad instance for your type myself:

data IFree f i j a where
    IReturn :: a -> IFree f i i a
    IFree :: f i j (IFree f j k a) -> IFree f i k a

instance IFunctor f => IFunctor (IFree f) where
    imap f (IReturn x) = IReturn (f x)
    imap f (IFree ff) = IFree $ imap (imap f) ff
instance IFunctor f => IMonad (IFree f) where
    ireturn = IReturn
    IReturn x >>>= f = f x
    IFree ff >>>= f = IFree $ imap (>>>= f) ff

We can build your Door monad for free from the following functor:

data DoorState = Opened | Closed
data DoorF i j next where
    Open :: next -> DoorF Closed Opened next
    Close :: next -> DoorF Opened Closed next
    Ring :: next -> DoorF Closed Closed next

instance IFunctor DoorF where
    imap f (Open x) = Open (f x)
    imap f (Close x) = Close (f x)
    imap f (Ring x) = Ring (f x)

type Door = IFree DoorF

open :: Door Closed Opened ()
open = IFree (Open (IReturn ()))
close :: Door Opened Closed ()
close = IFree (Close (IReturn ()))
ring :: Door Closed Closed ()
ring = IFree (Ring (IReturn ()))

You can open a door, which causes a currently-closed door to become open, close a currently-open door, or ring the bell of a door which remains closed, presumably because the house's occupant doesn't want to see you.

Finally, the RebindableSyntax language extension means we can replace the standard Monad class with our own custom IMonad.

(>>=) = (>>>=)
m >> n = m >>>= const n
return = ireturn
fail = undefined

door :: Door Open Open ()
door = do
    close
    ring
    open

However I notice that you're not really using the binding structure of your monad. None of your building blocks Open, Close or Ring return a value. So I think what you really need is the following, simpler type-aligned list type:

data Path g i j where
    Nil :: Path g i i
    Cons :: g i j -> Path g j k -> Path g i k

Operationally, Path :: (k -> k -> *) -> k -> k -> * is like a linked list, but it has some extra type-level structure, once again describing a path through a directed graph whose nodes are in k. The elements of the list are edges g. Nil says you can always find a path from a node i to itself and Cons reminds us that a journey of a thousand miles begins with a single step: if you have an edge from i to j and a path from j to k, you can combine them to make a path from i to k. It's called a type-aligned list because the ending type of one element must match the starting type of the next.

On the other side of Curry-Howard Street, if g is a binary logical relation then Path g constructs its reflexive transitive closure. Or, categorically, Path g is the type of morphisms in the free category of a graph g. Composing morphisms in the free category is just (flipped) appending type-aligned lists.

instance Category (Path g) where
    id = Nil
    xs . Nil = xs
    xs . Cons y ys = Cons y (xs . ys)

Then we can write Door in terms of Path:

data DoorAction i j where
    Open :: DoorAction Closed Opened
    Close :: DoorAction Opened Closed
    Ring :: DoorAction Closed Closed

type Door = Path DoorAction

open :: Door Closed Opened
open = Cons Open Nil
close :: Door Opened Closed
close = Cons Close Nil
ring :: Door Closed Closed
ring = Cons Ring Nil

door :: Door Open Open
door = open . ring . close

You don't get do notation (though I think RebindableSyntax does allow you to overload list literals), but building computations with (.) looks like sequencing of pure functions, which I think is a rather good analogy for what you're doing anyway. For me it requires extra brainpower - a rare and precious natural resource - to use indexed monads. It's better to avoid the complexity of monads when a simpler structure will do.

Benjamin Hodgson
  • 42,952
  • 15
  • 108
  • 157
  • Thanks for the detailed and very helpful answer. I suspect that I could use `RebindableSyntax` to actually get `do`-notation in the case of type-aligned list. – insitu Oct 23 '16 at 06:56
  • 2
    Note that the use of indexed monads in this (Bob Atkey) sense is good while all operations have a predictable effect on the state of the door. If you ever want to give some control away, e.g. to the people on the other side of the door (who might choose to open it or not), you'll benefit from something more flexible. – pigworker Oct 23 '16 at 10:02
  • Indeed. I have read this other SO post on indexed monads: http://stackoverflow.com/questions/28690448/what-is-indexed-monad and currently tackling your paper :) Is it possible to get same behaviour without HSE preprocessor? I should try by myself of course, but did not find time to do it yet and I suspect the answer is yes. – insitu Oct 23 '16 at 10:34
  • Sure. All SHE does is conceal the singleton encoding, to make things appear more like dependently typed programming. – pigworker Oct 23 '16 at 11:27
  • @insitu yeah, I guess you could abuse `RebindableSyntax` like that. But from an engineering perspective I don't think it's a very good idea to use `do` notation for things that aren't monads. – Benjamin Hodgson Oct 23 '16 at 13:04
  • @pigworker Out of curiosity, what do you make of [this post](https://www.eyrie.org/~zednenem/2012/07/29/paramonads) claiming that your and Atkey's indexed monads are equally powerful? I guess it's just a question of which is more natural to use in a given application? – Benjamin Hodgson Oct 23 '16 at 13:10
  • 2
    I note that the translation from monads on indexed sets to indexed monads on sets goes via continuations: i.e., by the introduction of extra control operators. When you use monads on indexed sets, you can stick with an "algebraic effects" style for more operations. – pigworker Oct 23 '16 at 13:14
  • 1
    @BenjaminHodgson Interestingly, that's the route taken by Idris: "Abused" do notations abound in the examples given in "Type Driven Development with Idris". I agree that, in Haskell, do-notation is so tightly knitted with monads that using it for something different might be asking for maintenance troubles. – insitu Oct 23 '16 at 15:18