This is all based on an idea that has been invented by a bunch of people independently, although in slightly different forms. I'm being chiefly guided by the paper Parameterized Notions of Computation.
The problem with the state monad is that it is defined by
newtype State s a = State { runState :: s -> (a, s) }
The state going into and out of one of these values is the same, s. We can't vary the type of the state as we pass through our code. But that's really easy to fix, just define:
> import Prelude hiding (return,(>>=),(>>),(.),id,drop)
> import Control.Category
> newtype State s1 s2 a = State { runState :: s1 -> (a, s2) }
I can now just copy and paste the definitions (with name changes to avoid clashes) out of the ghc prelude source code
> return' a = State $ \s -> (a, s)
> m >>>= k = State $ \s -> let
> (a, s') = runState m s
> in runState (k a) s'
> get = State $ \s -> (s, s)
> put s = State $ \_ -> ((), s)
We don't have to change a thing! The old code exactly matches the new type. We can now write code using the new State:
> test1 = return' 1 >>>= \x ->
> return' 2 >>>= \y ->
> get >>>= \z ->
> put (x+y*z) >>>= \_ ->
> return' z
> go1 = runState test1 10
But we're now also able to write code like:
> test2 = return' 1 >>>= \x ->
> return' 2 >>>= \y ->
> get >>>= \z ->
> put (show (x+y*z)) >>>= \_ ->
> return' z
> go2 = runState test2 10
The state starts of as an Integer but ends up as a String.
Problem solved! Except that this definition of State doesn't give us a monad and so we lose the benefits of having an interface shared by many monads. Is there a new more appropriate abstraction we can use? Rather than scratch our heads over it, we can just ask ghci to tell us what's going on.
*Main> :t return'
return' :: a -> State s1 s1 a
*Main> :t (>>>=)
(>>>=) :: State s1 s11 t -> (t -> State s11 s2 a) -> State s1 s2 a
This immediately suggests a new abstraction:
> class ParameterisedMonad m where
> return :: a -> m s s a
> (>>=) :: m s1 s2 t -> (t -> m s2 s3 a) -> m s1 s3 a
> x >> f = x >>= \_ -> f
It's a lot like the usual Monad class except that we're now parameterising uses of this class with a pair of types. Our new >>= operator also has a compatibility condition on it. We can think of an element of m s1 s2 as having a 'tail' and 'head' living in s1 and s2 respectively. In order to use >>= we require the head of the first argument to match the tail given by the second argument.
Anyway, we have:
> instance ParameterisedMonad State where
> return = return'
> (>>=) = (>>>=)
We didn't really design this class, we just used what ghci told us. Will it turn out to be a useful abstraction?
First a category theoretical aside: in this post I talked about how monads were really a kind of abstract monoid. Well ParameterisedMonad is a kind of abstract category. If we were to implement join for this class it would play a role analogous to composition of arrows in a category. In a monoid you can multiply any old elements together to get a new element. In a category, you can't multiply two arrows together unless the tail of the second matches the head of the first.
Now we can generalise the writer monad to a ParameterisedMonad. But there's a twist: every monoid gives rise to a writer. This time we'll find that every category gives rise to a ParameterisedMonad. Here's the definition. Again, it was lifted straight out of the source for the usual Writer monad. The main change is replacing mempty and mappend with id and flip (.).
> data Writer cat s1 s2 a = Writer { runWriter :: (a,cat s1 s2) }
> instance (Category cat) => ParameterisedMonad (Writer cat) where
> return a = Writer (a,id)
> m >>= k = Writer $ let
> (a, w) = runWriter m
> (b, w') = runWriter (k a)
> in (b, w' . w)
> tell w = Writer ((),w)
> execWriter m = snd (runWriter m)
It's just like the usual Writer monad except that the type of the 'written' data may change. I'll borrow an example (modified a bit) from the paper. Define some type safe stack machine operations that are guaranteed not to blow your stack:
> push n x = (n,x)
> drop (_,x) = x
> dup (n,x) = (n,(n,x))
> add (m,(n,x)) = (m+n,x)
> swap (m,(n,x)) = (n,(m,x))
We can now 'write' the composition of a bunch of these operations as a 'side effect':
> test3 = tell (push 1) >>
> tell (push 2) >>
> tell dup >>
> tell add >>
> tell swap >>
> tell drop
> go3 = execWriter test3 ()
I guess there's one last thing I have to find. The mother of all parameterised monads. Again, we lift code from the ghc libraries, this time from Control.Monad.Cont. I just tweak the definition ever so slightly. Normally when you hand a continuation to an element of the Cont type it gives you back an element of the continuation's range. We allow the return of any type. This time the implementations of return and (>>=) remain completely unchanged:
> newtype Cont r1 r2 a = Cont { runCont :: (a -> r2) -> r1 }
> instance ParameterisedMonad Cont where
> return a = Cont ($ a)
> m >>= k = Cont $ \c -> runCont m $ \a -> runCont (k a) c
> i x = Cont (\fred -> x >>= fred)
> run m = runCont m return
> test4 = run $ i (tell (push 1)) >>
> i (tell (push 2)) >>
> i (tell dup) >>
> i (tell add) >>
> i (tell swap) >>
> i (tell drop)
> go4 = execWriter test4 ()
So what's going on here? The implementations of these instances require almost trivial changes to the original monads, or in two cases no changes at all apart from the type signature. I have my opinion: Haskell programmers have been using the wrong type class all along. In each case the type signature for return and >>= was too strict and so the functionality was being unnecessarily shackled. By writing the code without a signature, ghci tells us what the correct signature should have been all along. I think it might just possibly be time to consider making ParameterisedMonad as important as Monad to Haskell programming. At the very least, do-notation needs to be adapted to support ParameterisedMonad.
Update: You *can* use do-notation with ParameterisedMonad if you use the NoImplicitPrelude flag.
Update2: Some credits and links:
- The Polystate Monad is one of the independent discoveries I mentioned above.
- A more general approach to Parameterized Monads in Haskell.
- A comment on Parameterized Monads that shows explicitly how to make this work with NoImplicitPrelude.
- Oleg's Variable (type)state `monad'.
- Wadler discovered this design pattern back in 1993 in Monads and composable continuations.
I didn't contribute anything, this article is just advocacy.
Nice observation. Certainly, one major reason why parameterized monads are so attractive is exactly because of the easy upgrade path. Anybody who is comfortable writing monadic code can get started very easily, and allow the typechecker to do the heavy mental lifting for them.
ReplyDeleteHere lies a dependently typed version: http://www.cs.nott.ac.uk/~nad/repos/lib/Category/Monad/Indexed.agda
ReplyDeleteHere lies a dependently typed version: http://www.cs.nott.ac.uk/~nad/repos/lib/Category/Monad/Indexed.agda
ReplyDeleteSo shouldn't they be called arrowads instead since they are no longer abstract monoids?
ReplyDeleteTo get do-notation, can't you just use NoImplicitPrelude?
ReplyDeleteParametrizedMonad is indeed a very useful abstraction. But it makes me wonder what happens to type inference and to error messages. I just don't know the answer.
ReplyDeleteCan the extra flexibility cause ambiguities?
What happens when one makes an error? Might it still type check? Or give an even stranger type error?
Dan, have you tried to use do-notation with this definition of Parameterised Monads ? If so, what was the problem? Since GHC supports rebindable syntax, do-notation should just work when throwing in a NoImplicitPrelude pragma.
ReplyDeleteThis is related to a (wilder) notion of parameterised monads that Edward Kmett gave some time ago.
I agree. I always wanted monads to be more flexible, but as not so experienced Haskell programmer I thought it is something wrong with me since hackers of Haskell would have seen it a long time ago, wouldn't they? I feel relieved (a little bit ^^). Thank you sigfpe!
ReplyDeleteJust to note that some Haskell programmers were using this extended notion of ParametrisedMonad even before the do-notation was standardised. For instance, back in 1994-5, Niklas Röjemo used this idiom extensively inside the nhc12 and nhc13 compilers. See here for evidence.
ReplyDeleteMIND = BLOWN
ReplyDeleteThis is awesome stuff!
"At the very least, do-notation needs to be adapted to support ParameterisedMonad. "
ReplyDeleteThis is already possible in Ghc using -XNoImplicitPrelude or the corresponding LANGUAGE pragma.
---
Just for fun, let me try to continue with your idea. Instead of taking (>>=) as a primitive, I prefer fmap, join and Applicative stuff. So:
> class ParameterisedFunctor m where
> fmap :: (a -> b) -> (m s s a) -> (m s s b)
>
> class ParameterisedFunctor m => ParameterisedPointed m where
> pure :: a -> m s s a
>
> class ParameterisedPointed m => ParameterisedApplicative m where
> (<*>) :: m s1 s2 (a -> b) -> m s2 s3 a -> m s1 s3 b
>
> class ParameterisedApplicative m => ParameterisedMonad m where
> join :: m s1 s2 (m s2 s3 a) -> m s1 s3 a
Now take the ParameterisedApplicative class. For normal Applicatives there is a Dual instance that reverses the order of effects, i.e.:
> (<**>) :: m a -> m (a -> b) -> m b
Here the reversed ordering is not clear from the type. However, with ParameterizedApplicative:
> (<**>) :: m s1 s2 a -> m s2 s3 (a -> b) -> m s1 s3 b
We do see the ordering of effects in the type.
---
Also, the type "m s1 s2 a" reminds me a lot of the types of Control.Category. Is there something equivalent to the Kleisli wrapper?
> newtype Kleisli m a b = Kleisli
(a -> m b)
GHC does support do-notation for parameterised monads. With GHC 6.10 (at least) you merely need to use {-# LANGUAGE NoImplicitPrelude #-}
ReplyDeleteand GHC will resolve do-notation to use whatever is in scope for (>>=), (>>) and "return". Parameterised monads are also used in Oleg et al's lightweight static resources paper.
GHC supports do-notation for parameterized monads just fine, as it turns out.
ReplyDeleteI tried to write some code here to show you how to do it but Blogger doesn't allow me to use the proper tags when commenting so it will just look like crap. Instead here is a link to my blog which shows how to do it.
Augustss: My experience has been both. Sometimes you do get impenetrable error messages; often because some definition you got wrong a page or two of code ago actually does typecheck, whereas it wouldn't have previously.
ReplyDeleteI quickly learned to frequently use ghci to infer the most general type of new definitions. If the type appeared to be right, I'd copy and paste it into my code, if it was obviously wrong, I'd try again. When problems arose, sometimes it took a fair bit of mental effort to figure out exactly what they were.
There is definitely higher type overhead, at least in the short run. But it puts the type system to better work _for you_, among other compelling advantages.
augustss,
ReplyDeleteIt'd be easy to test to see what many of the consequences might be: try recompiling a non-trivial body of code with a parameterised monad library.
My main concern is with monad transformers. The types of the parameterised version will carry a lot of information. We could start getting some pretty big signatures.
I've been mucking about with functors between slice categories. Er, um,
ReplyDeletetype x :->: y = forall i. x i -> y i
class IFunctor (t :: (* -> *) -> * -> *) where
imap :: (x :->: y) -> (t x :->: t y)
To me this suggests.
class IMonad (t :: (* -> *) -> * -> *) where
iret :: x :->: t x
ibind :: (x :->: t y) -> (t x :->: t y)
Can we get back these domino-style parametrized monads?
Seems so.
data ILike :: * -> * -> * -> * where
ILike :: x -> ILike x i i
type Domino t i j x = t (ILike x j) i
dret :: IMonad t => x -> Domino t i i x
dret = iret . ILike
iTweak :: (x -> Domino t j k y) -> (ILike x j :->: t (ILike y k))
iTweak f (ILike x) = f x
dbind :: IMonad t => (x -> Domino t j k y) -> Domino t i j x -> Domino t i k y
dbind f b = ibind (iTweak f) b
Not sure where I'm going with this. Just thought I'd mention it. I detect traces of Dr Hancock...
Parametrized monads were also used in a Haskell Workshop 2008 paper with the name "class Monadish".
ReplyDeleteAn implementation of the concept is available at http://hackage.haskell.org/cgi-bin/hackage-scripts/package/Coroutine
Ryan,
ReplyDeleteI think "Indexed monad" and IxMonad are much better names. I'll check out Tov's session types paper.
A category object in Set consists of:
ReplyDelete- two sets O and M (objects and morphisms)
- a function i:O -> M (picks out the identity morphism)
- two functions s,t:M -> O (source and target)
- a function o:X -> M (composition), where X is a set equipped with two functions i,j:X -> M such that s o i = t o j (that is, it's the set of composable pairs and i,j pick out the first or second element)
such that the usual relations hold.
Category objects can be defined in any category with pairwise pullbacks:
A category object in C consists of
- two objects O, M
- a morphism i:O -> M
- two morphisms s, t:M -> O
- a morphism o:X -> M where X is equipped with two morphisms i,j:X -> M s.t. s o i = t o j
such that the usual relations hold.
The generalization you describe is as follows:
A "parametrized monad" consists of
- two sets O, M (where the elements of O are categories and the elements of M are functors between them)
- a function i:O -> M (picking out the identity functor on each category)
- two functions s,t:M -> O (source and target of the functors)
- a function o:X -> M (composition of functors)
But this is just a subcategory of Cat! What's going on? Just as a monoid is a one-object category, a monoidal category is a one-object 2-category. The generalization you describe moves from the monoidal category End(C) to a 2-category with more objects. So the real term you want isn't "parameterized monad", it's "sub-2-category of Cat".
A different generalization would be to consider a category object in End(C):
A category object in End(C) consists of
- two functors O, M
- a natural transformation i:O -> M
- two natural transformations s, t:M -> O
- a natural transformation o:X -> M where the functor X is equipped with two natural transformations i,j:X -> M s.t. s o i = t o j
such that the usual relations hold.
Can you come up with a use case for this one?
mike,
ReplyDeleteHaven't digested all of your comment yet. But briefly, a category object in End(C) is exactly what I'm thinking of. But I haven't expanded out the full definition myself to ensure all of the maps are exactly what they need to be.
Better late than never :P
ReplyDeletehttp://groups.google.com/group/comp.lang.haskell/browse_thread/thread/98384971db91201e/44a51f58ebb6c6ad#44a51f58ebb6c6ad
sigfpe,
ReplyDeleteIn a normal monad, bind has type
m a -> ( a -> m b ) -> m b
where m:C -> C is a functor and a,b in C.
Your bind has type
( m s1 s2 ) a ->
( a -> (m s1 s2) b ) -> ( m s1 s2 ) b,
where
a,b in C,
s1, s2 in S,
and (ostensibly)
m(s1, s2):C -> C.
So you have a different functor from C to C for each pair (s1, s2) in S. However, you also forbid composition of such functors unless the types match, so the functors aren't really from C to C, they're from s1 x C to s2 x C.
This means you can't possibly be describing a category object in End(C). You have a 2-category whose objects are copies of C indexed by elements of S, functors between the categories, and natural morphisms between the functors.
It's a lovely construction, and well illustrated, just not the one you might have thought you were getting.
mike,
ReplyDeleteI still haven't got around to thinking for myself about what this construction is from a category theoretical perspective. End(C) was just a guess based on ordinary monads being monoids. I'll have to return to this when I have a moment.
Cool. I guess this can be described as a type-level-state monad?
ReplyDeleteYair,
ReplyDeleteThis does not represent the value of the state variable inside the type system; rather it allows the type of state variable to vary over the course of the computation.
If you can imagine an example where you want to store two possible types, say either an integer or a string, in the state variable, then one way to do it is to use State (Either Integer String) a. If you know whether the state variable is a string or an integer based solely on where you are in the computation, you can use the parameterized state monad in this post, and avoid the use of Either.