9

In a previous question I discovered the existence of Conor McBride's Kleisli arrows of Outrageous Fortune while looking for ways of encoding Idris examples in Haskell. My efforts to understand McBride's code and make it compile in Haskell led to this gist: https://gist.github.com/abailly/02dcc04b23d4c607f33dca20021bcd2f

While searching on Hackage, I discovered several implementations of these concepts, notably by (guess who?) Edward Kmett and Gabriel Gonzalez.

What experience do people have putting such code in production? In particular, do the expected benefits (runtime safety, self-guiding usage) actually occur IRL? How about maintaining this kind of code over time and onboarding newcomers?

EDIT: I changed the title to be more explicit about what I am looking for: Real-world use of indexed monads in the wild. I am interested in using them and I have several use-cases in mind, just wondering if other people have already used them in "production" code.

EDIT 2: Thanks to the great answers provided so far and helpful comments, I edited that question's title and description again to reflect more precisely what kind of answer I expect, e.g. experience report.

Community
  • 1
  • 1
insitu
  • 4,488
  • 3
  • 25
  • 42
  • e.g. here https://www.youtube.com/watch?v=60gUaOuZZsE, it almost made into a library, but wasn't worth the complexity. – phadej Nov 06 '16 at 21:50
  • I would expect that applications of indexed monads would more likely be found in total languages, like Agda and Coq. In Haskell, you can get away with using partial functions that you know will be total in context; in those languages you can't. *Needing* an indexed monad will go a long way toward convincing you that it's worth the trouble to work with one. – dfeuer Nov 06 '16 at 23:14
  • 1
    @dfeuer I can imagine various applications of this technique in my current code, like enforcing correct state transitions at the type level in protocols. I agree that you can "know" your functions are total in a context, but when coding for the long term, esp. in a team setting, explicit trumps implicit so I would rather have a mechanism to help people be aware of those assumptions on context. One particular area that springs to my mind is system configuration, like what propellor does. – insitu Nov 07 '16 at 07:06
  • You may have already seen my question, but Conor tried to model a DVD ejection example here: http://stackoverflow.com/a/28696299/1651941 – Sibi Nov 07 '16 at 08:35
  • Code that's actually being used in production? As in, a link to some source code? I think that's [off-topic](http://stackoverflow.com/help/on-topic) ("Questions asking us to recommend or find... [an] off-site resource are off-topic"). Besides, I'd expect most of such applications to be closed-source and therefore not shareable here. – Benjamin Hodgson Nov 07 '16 at 15:30
  • 2
    I think you should consider changing your question to ask for self-contained examples of practical uses for indexed monads. Stack Overflow is designed for self-contained code-centric posts. Note that all three of the answers you've got so far are answering _that_ question, not the question you directly asked. – Benjamin Hodgson Nov 07 '16 at 15:33
  • @BenjaminHodgson I noticed that :) My question is probably badly formulated and all the answers are really interesting potential applications of indexed monads to real-world problems. Actually, I am pretty much sold on the idea of using indexed monads and I was looking for other people's experience using such kind of "advanced" constructs. In particular, I am interested in how it can be maintained and the ease of onboarding other devs... – insitu Nov 07 '16 at 15:58

3 Answers3

4

Session types are an attempt to give type-level descriptions to networking protocols. The idea is that if a client sends a value, the server must be ready to receive it, and vice versa.

So here's a type (using TypeInType) describing sessions consisting of a sequence of values to send and values to receive.

infixr 5 :!, :?
data Session = Type :! Session
             | Type :? Session
             | E

a :! s means "send a value of type a, then continue with the protocol s". a :? s means "receive a value of type a, then continue with the protocol s".

So Session represents a (type-level) list of actions. Our monadic computations will work their way along this list, sending and receiving data as the type demands it. More concretely, a computation of type Chan s t a reduces the remaining work to be done to satisfy the protocol from s to t. I'll build Chan using the indexed free monad that I used in my answer to your other question.

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


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 fx) = IFree (imap (imap f) fx)

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

Our base actions in the Chan monad will simply send and receive values.

data ChanF s t r where
    Send :: a -> r -> ChanF (a :! s) s r
    Recv :: (a -> r) -> ChanF (a :? s) s r

instance IFunctor ChanF where
    imap f (Send x r) = Send x (f r)
    imap f (Recv r) = Recv (fmap f r)

send :: a -> Chan (a :! s) s ()
send x = IFree (Send x (IReturn ()))

recv :: Chan (a :? s) s a
recv = IFree (Recv IReturn)

type Chan = IFree ChanF
type Chan' s = Chan s E  -- a "complete" Chan

send takes the current state of the session from a :! s to s, fulfilling the obligation to send an a. Likewise, recv transforms a session from a :? s to s.

Here's the fun part. When one end of the protocol sends a value, the other end must be ready to receive it, and vice versa. This leads to the idea of a session type's dual:

type family Dual s where
    Dual (a :! s) = a :? Dual s
    Dual (a :? s) = a :! Dual s
    Dual E = E

In a total language Dual (Dual s) = s would be provable, but alas Haskell is not total.

You can connect a pair of channels if their types are dual. (I guess you'd call this an in-memory simulation of connecting a client and a server.)

connect :: Chan' s a -> Chan' (Dual s) b -> (a, b)
connect (IReturn x) (IReturn y) = (x, y)
connect (IReturn _) (IFree y) = case y of {}
connect (IFree (Send x r)) (IFree (Recv f)) = connect r (f x)
connect (IFree (Recv f)) (IFree (Send y r)) = connect (f y) r

For example, here's a protocol for a server which tests whether a number is greater than 3. The server waits to receive an Int, sends back a Bool, and then ends the computation.

type MyProtocol = Int :? Bool :! E

server :: Chan' MyProtocol ()
server = do  -- using RebindableSyntax
    x <- recv
    send (x > 3)

client :: Chan' (Dual MyProtocol) Bool
client = do
    send 5
    recv

And to test it:

ghci> connect server client
((),True)

Session types are an area of active research. This particular implementation is fine for very simple protocols, but you can't describe protocols where the type of the data being sent over the wire depends on the state of the protocol. For that you need, surprise surprise, dependent types. See this talk by Brady for a quick demo of the state of the art of session types.

Benjamin Hodgson
  • 42,952
  • 15
  • 108
  • 157
  • 1
    Are you sure full dependent types are required, and not just McBride-style indexed monads with singletons? Singletons+`TypeInType` are awfully powerful, though painful. – dfeuer Nov 07 '16 at 17:48
  • @dfeuer That's a good question to which I don't know the answer; I haven't tried cranking the handle on [resource-dependent effects](https://eb.host.cs.st-andrews.ac.uk/drafts/dep-eff.pdf) in Haskell. I can't imagine it'll be pretty – Benjamin Hodgson Nov 07 '16 at 18:08
3

I think the below should count as a practical example: statically enforcing "well-stackedness" in a compiler. Boilerplate first:

{-# LANGUAGE GADTs, KindSignatures #-}
{-# LANGUAGE DataKinds, TypeOperators #-}
{-# LANGUAGE RebindableSyntax #-}

import qualified Prelude
import Prelude hiding (return, fail, (>>=), (>>))

Then a simple stack language:

data Op (i :: [*]) (j :: [*]) where
    IMM :: a -> Op i (a ': i)
    BINOP :: (a -> b -> c) -> Op (a ': b ': i) (c ': i)
    BRANCH :: Label i j -> Label i j -> Op (Bool ': i) j

and we won't bother with real Labels:

data Label (i :: [*]) (j :: [*]) where
    Label :: Prog i j -> Label i j

and Programs are just type-aligned lists of Ops:

data Prog (i :: [*]) (j :: [*]) where
    PNil :: Prog i i
    PCons :: Op i j -> Prog j k -> Prog i k

So in this setting, we can very easily make a compiler which is an indexed writer monad; that is, an indexed monad:

class IMonad (m :: idx -> idx -> * -> *) where
    ireturn :: a -> m i i a
    ibind :: m i j a -> (a -> m j k b) -> m i k b

-- For RebindableSyntax, so that we get that sweet 'do' sugar
return :: (IMonad m) => a -> m i i a
return = ireturn
(>>=) :: (IMonad m) => m i j a -> (a -> m j k b) -> m i k b
(>>=) = ibind
m >> n = m >>= const n
fail = error

that allows accumulating a(n indexed) monoid:

class IMonoid (m :: idx -> idx -> *) where
    imempty :: m i i
    imappend :: m i j -> m j k -> m i k

just like regular Writer:

newtype IWriter w (i :: [*]) (j :: [*]) (a :: *) = IWriter{ runIWriter :: (w i j, a) }

instance (IMonoid w) => IMonad (IWriter w) where
    ireturn x = IWriter (imempty, x)
    ibind m f = IWriter $ case runIWriter m of
        (w, x) -> case runIWriter (f x) of
            (w', y) -> (w `imappend` w', y)

itell :: w i j -> IWriter w i j ()
itell w = IWriter (w, ())

So we just apply this machinery to Programs:

instance IMonoid Prog where
    imempty = PNil
    imappend PNil prog' = prog'
    imappend (PCons op prog) prog' = PCons op $ imappend prog prog'

type Compiler = IWriter Prog

tellOp :: Op i j -> Compiler i j ()
tellOp op = itell $ PCons op PNil

label :: Compiler i j () -> Compiler k k (Label i j)
label m = case runIWriter m of
    (prog, ()) -> ireturn (Label prog)

and then we can try compiling a simple expression language:

data Expr a where
    Lit :: a -> Expr a
    And :: Expr Bool -> Expr Bool -> Expr Bool
    Plus :: Expr Int -> Expr Int -> Expr Int
    If :: Expr Bool -> Expr a -> Expr a -> Expr a

compile :: Expr a -> Compiler i (a ': i) ()
compile (Lit x) = tellOp $ IMM x
compile (And x y) = do
    compile x
    compile y
    tellOp $ BINOP (&&)
compile (Plus x y) = do
    compile x
    compile y
    tellOp $ BINOP (+)
compile (If b t e) = do
    labThen <- label $ compile t
    labElse <- label $ compile e
    compile b
    tellOp $ BRANCH labThen labElse

and if we omitted e.g. one of the arguments to BINOP, the typechecker will detect this:

compile (And x y) = do
    compile x
    tellOp $ BINOP (&&)
  • Could not deduce: i ~ (Bool : i) from the context: a ~ Bool
Cactus
  • 27,075
  • 9
  • 69
  • 149
  • 1
    I like the example and find it both interesting and enlightening. Is this actual code used in production in a compiler or "just" some potential application of indexed monads? I tried to make my question more precise... – insitu Nov 07 '16 at 08:50
  • 1
    Nah I wrote it just for this answer. But I could very easily imagine using it in some prod code. – Cactus Nov 07 '16 at 08:59
  • Why not `newtype Label i j = Label (Prog i j)`? – dfeuer Nov 07 '16 at 17:52
  • @dfeuer: I wanted to emphasize that it should be something completely different than an inlined `Prog i j` – Cactus Nov 08 '16 at 02:13
  • Yeah, that makes sense. – dfeuer Nov 08 '16 at 06:15
2

Another nice example is mutexes with lock-unlock check at compile time. You can find this example on Stephen Diehl website:

http://dev.stephendiehl.com/hask/#indexed-monads

Shersh
  • 9,019
  • 3
  • 33
  • 61
  • Nice example too... But once again, I am looking for code that's in production somewhere :) – insitu Nov 07 '16 at 14:52
  • 2
    @insitu I'm not aware about use of indexed monad in production. (1) there is not so much Haskell code in production; (2) it requires `-XRebindableSyntax` if you want to use this monads with `do` (and you want) but this extension is not so common; (3) there are not so many people who understand such concepts well and not many among them need to write production code which uses such monads. Though maybe in future of Haskell these ideas will become more popular :) – Shersh Nov 07 '16 at 18:00