monad transformers
So, I create my own monad transformers quite often, It is great to be able to compose 5 or 6 transformers to get the perfect one for a given task. for example here is one for collecting statistics: -- The stats newtype Stat = Stat (Map.Map Atom Int) instance Monoid Stat where mempty = Stat Map.empty mappend (Stat a) (Stat b) = Stat $ Map.unionWith (+) a b -- The Monad transformer. newtype StatT m a = StatT (WriterT Stat m a) deriving(MonadIO, Functor, MonadTrans, Monad) runStatT (StatT m) = runWriterT m -- The operations on this type of monad class Monad m => MonadStats m where mticks' :: Int -> Atom -> m () instance Monad m => MonadStats (StatT m) where mticks' n k = StatT $ tell (Stat $ Map.single k n) Now all is well and good, I was able to use newtype deriving for most every interesting property I wanted from the WriterT monad. (note I specifically did not derive MonadWriter, as the point of my own transformer is to hide this). However, now we come to the crux, I want the various interesting monad operations (MonadReader, MonadWriter, etc.. ) to be able to pass through StatT as well as let MonadStats pass through other monad transformers. the second is easy and elegant: instance (Monad m, Monad (t m), MonadTrans t, MonadStats m) => MonadStats (t m) where mticks' n k = lift $ mticks' n k the first, however, is tricky. Right now, it appears that every Monad type in the libraries defines a rule for commuting with every other monad type, this seems impracticle for many monads (n^2 rules!) so, my question is, why arn't the standard monad transformers declared with an instance like above? as in: instance (Monad m, Monad (t m), MonadTrans t, MonadWriter w m) => MonadWriter w (t m) where ... Then, all that is needed is for everyone to declare theire monads as MonadTrans providing an appropriate lift and every monad property should compose nicely.* Another semi-solution would be to allow newtype-deriving of these monad properties somehow.. newtype StatT m a = StatT (WriterT Stat m a) deriving(MonadIO, Functor, MonadTrans, Monad, MonadReader) doesn't work, how bout something like newtype StatT m a = StatT (WriterT Stat m a) deriving(MonadIO, Functor, MonadTrans, Monad, MonadReader r m => MonadReader r ) which should give the newtype-deriving rule enough to figure out how to derivie the MonadReader rule properly. * We might need to come up with a more advanced MonadTrans class for certain monads, as 'lift' has been shown to not be fully capable for some situtations such as catching exceptions in an arbitrary MonadIO. ** The monad transformer libraries appear to have moved somewhere in the most recent cvs fptools tree, anyone know where they moved too? *** Monad transformers rock. -- John Meacham - ⑆repetae.net⑆john⑈
G'day all. Quoting John Meacham <john@repetae.net>:
However, now we come to the crux, I want the various interesting monad operations (MonadReader, MonadWriter, etc.. ) to be able to pass through StatT [...]
Why do you want to do that? Think carefully before you answer. I've never liked the O(n^2) instances. First off, it's unmaintable. Secondly, it's extremely fragile (all of your carefully-written code which uses "read" is going to fail when you need to stack on another Reader for some small part of the code). Thirdly, and most importantly, the "raw" monad operations are almost certainly meaningless and sometimes are misleading. Your "mticks" operation is a perfect example of a good operation. That means something that a mere "tell" does not. Indeed, when you have "mticks", you don't need "tell" at all. So, quite correctly, you don't expose it. Why should things be any different for the other raw monad operations? There are some exceptions, of course. I think that MonadIO, for example, is a very reasonable class to expose because it's unambiguous, meaningful and robust. OK, now here's the way that I do it. First, some state values: data RState = {- detail omitted -} data WState = {- detail omitted -} newtype CounterState = CounterState Int -- Example instance Monoid WState where {- detail omitted -} Now we can create our monad: type MyMReader = ReaderT RState IO type MyMCounter = StateT RWState MyMReader type MyMWriter = WriterT WState MyMCounter newtype MyM a = MyM (MyMWriter a) deriving (Monad, MonadIO) And some internal lift operations (not exported to the MyM clients!): liftMyMtoWriter :: MyMWriter a -> MyM a liftMyMtoWriter = MyM liftMyMtoCounter :: MyMCounter a -> MyM a liftMyMtoCounter = liftMyMtoWriter . lift liftMyMtoReader :: MyMReader a -> MyM a liftMyMtoReader = liftMyMtoCounter . lift Then you can define your meaningful operations: incrementCounter :: MyM Int incrementCounter = liftMyMtoCounter (do RWState x <- get put (RWState (x+1)) return x ) Using the internal lift operations and exposing only meaningful operations gives both you and the client insulation against the transformer stack changing, which it will. As an added bonus, it lets you define your own "lift" instance if that makes sense: type MyMReader m = ReaderT RState m type MyMCounter m = StateT RWState (MyMReader m) type MyMWriter m = WriterT WState (MyMCounter m) newtype MyM m a = MyM (MyMWriter m a) deriving (Monad, MonadIO) liftMyMtoWriter :: (Monad m) => MyMWriter m a -> MyM m a liftMyMtoWriter = MyM liftMyMtoCounter :: (Monad m) => MyMCounter m a -> MyM m a liftMyMtoCounter = liftMyMtoWriter . lift liftMyMtoReader :: (Monad m) => MyMReader m a -> MyM m a liftMyMtoReader = liftMyMtoCounter . lift liftMyMtoBase :: (Monad m) => m a -> MyM m a liftMyMtoBase = liftMyMtoReader . lift instance MonadTrans MyM where lift = liftMyMtoBase
*** Monad transformers rock.
Yes they do, but never forget that they're building blocks for monads which have meaning on their own, independent of implementation. Cheers, Andrew Bromage
Hello, (appologies --- as I was writing, my post digressed from your original point, I hope the discussion is still interesting though :-) On Sat, 29 Jan 2005 18:48:00 -0800, John Meacham <john@repetae.net> wrote:
... instance (Monad m, Monad (t m), MonadTrans t, MonadStats m) => MonadStats (t m) where mticks' n k = lift $ mticks' n k
the first, however, is tricky. Right now, it appears that every Monad type in the libraries defines a rule for commuting with every other monad type, this seems impracticle for many monads (n^2 rules!) so, my question is, why arn't the standard monad transformers declared with an instance like above?
The library defines the interaction of every transformer with every other one. If you think of monad transformers as "language features", we are simply saying how each features interacts with every other one (this is why we have to be careful when we introduce new language features :-). Not all features are orthogonal (i.e. they don't all commute). In my experience, the features that don't commute are the ones that somehow tend to affect the control flow of the program, e.g. exception handling, backtracking, explicit continuations. Now for many transformers the interaction with other ones is "straight forward" and can be captured with a declaration like the above. Such declarations lead to overlapping instances, usually you need to rules, e.g. instance ReaderM (ReaderT r m) r where ... instance (ReaderM m r, Trans t) => ReaderM (t m) r where ... The overlapping instances are nice in this case, as they cut down on the code a lot. However the interaction of overlapping instances and functional dependencies is a bit unclear (at least in my mind). This leads to the question" "Should functional dependencies be used?". I have heard arguments both for, and against using them. The question is "can a monad be a reader in more than one way?". Clearly we can make such moands (use ReaderT twice). But then, when we say "Get the environment", which one do we mean? With functional dependencies the answer (usually) is: get the outermost environment, and you have to do something special (explicit lifting) to access the other ones. Without fun.deps. the answer is: try to figure out which one we mean, based on the expected type of the value. Often that leads to ambiguities, but of course that can be fixed by writing types. Neither of the approaches is particularly satisfying: the point is that if we are going to have more than one of something, then different things should have names, so that we can specify whcih one we mean. With fun. deps. we only have a single name (the putermost one), without them, we are using the types of things for their names. Both of these are unsatisfactory. It is possible in Haskell to code up a name like system --- I wrote a library like that once, and it is on my web page (www.cse.ogi.edu/~diatchki). It involves the (by now usual) class hackery and it is not real pretty. Essentially, classes get another parameter (the name of the layer), and there is functional dependenciy between the monad and the name, but not the type, e.g. you get constraints like: ReaderM m x t saying: "the monad 'm' has a read-only field called 'x' of type 't'". Sadly you cannot pick arbitrary names for your variables, instead the variable names are natural numbers. Note that now the functional dependency makes sense --- it says that we should not have variables with the same name, but we can have multple variables of the same type. A language feature avoiding such hackery would be nice. By the way, note that having multiple layers of the same monad is really useful sometimes and the layers cannot be colapsed into a single layer with a tuple. For example: StateT s1 (ExceptT err1 (StateT s2 m)) We cannot colapse the two state layers into a single one. Raising an 'err1' error undoes the changes to state variable 's1', but not the changes to variable 's2'.
** The monad transformer libraries appear to have moved somewhere in the most recent cvs fptools tree, anyone know where they moved too? I am not sure where "the standard" library is. There are some transformers under: libraries/monads. They are very similar to the ones in the standard library. The ones in CVS are my working version, there is a link to a "stable" one (yeah right :-) on my web page (monadLib), and the next realease should appear there sometime this week (maybe even today). -Iavor
participants (3)
-
ajb@spamcop.net -
Iavor Diatchki -
John Meacham