A collection of related proposals regarding monads
I personally feel that the inclusion of 'fail' in the Monad class is an ugly solution to the problem of pattern matching, and gives the incorrect impression that monads should have some builtin notion of failure. Indeed, it's becoming common to type the result of some function in an arbitrary monad in order to indicate the potential for failure, which is strictly speaking, not the right thing to do. (In a lot of cases, it's going to be no better than complete program failure) We ought to be using MonadZero when we want to express failure, but it's gone! What do people think of the following proposal? Remove fail from the Monad class. Reinstate MonadZero as a separate class as in Haskell 1.4. Do notation is translated as in Haskell 1.4: do {e} = e do {e;stmts} = e >> do {stmts} -- if p is refutable, do {p <- e; stmts} = let ok p = do {stmts}; ok _ = mzero in e >>= ok -- if p is irrefutable, do {p <- e; stmts} = e >>= \p -> do {stmts} do {let decls; stmts} = let decls in do {stmts} Thus, refutable pattern matches occuring in a do-expression would force the expression to be typed explicitly with MonadZero. This is probably a good thing, as it forces one to think about the consequences of not properly dealing with a pattern match in any monad which doesn't explicitly allow for failure. Even if this translation of do-syntax isn't accepted, I still think that we should have a separate MonadZero. Its existence lets various type signatures become more expressive. There are a lot of cases where a MonadPlus constraint pops up where a MonadZero constraint would do. (I've also been seeing Monad get used for these cases, when it shouldn't!) This distinction allows one to see immediately when a monad is getting used for failure or whether choice is really essential. I'd also like to see the current use of MonadPlus split into MonadElse (or MonadOr) and MonadPlus, as described at the bottom of http://www.haskell.org/hawiki/MonadPlus as it helps to clarify the distinction between backtracking-type failure and immediate failure in types. We could even put this distinction to good use in many monads which do support backtracking anyway: instance MonadElse [] where [] `morelse` ys = ys (x:xs) `morelse` ys = (x:xs) Lastly, it would be nice to have some standard name for the function: option :: (MonadPlus m) => [a] -> m a option = foldr (mplus . return) mzero which seems to come up quite a bit in my experience with nondet monads. - Cale P.S. Oh, and don't get me started about the whole Functor subclass thing, and the inclusion of join in the Monad class. Of course I want those too. :) P.P.S. This reopens the possibility for monad comprehensions, and the more general versions of filter, concat, etc. in the 1.4 prelude -- I wasn't actually around for Haskell 1.4, but I *really* think they'd be a nice language feature to have, as I like to view monads as an abstraction of *containers*, which that syntax/methodology emphasises. I usually find that initially teaching monads to newcomers in terms of containers is much more effective, and that syntax would allow for a nice segue from lists to monads. If people think the error messages are scary, we could provide a switch -fbeginner and a more 98-like prelude to turn monad comprehensions and any other potentially scary features off for the newcomers.
G'day Cale. Quoting Cale Gibbard <cgibbard@gmail.com>:
I personally feel that the inclusion of 'fail' in the Monad class is an ugly solution to the problem of pattern matching, and gives the incorrect impression that monads should have some builtin notion of failure.
So do I.
We ought to be using MonadZero when we want to express failure, but it's gone!
I agree!
What do people think of the following proposal?
I don't like it. My feeling is that do { p <- xs; return e } should behave identically (modulo the precise error message if the pattern match fails) to map (\p -> e) xs. Your proposal would make it into a map/filter hybrid. Speaking more practically, if a pattern match is technically refutable, but I, as the programmer, intend it never to fail, then I shouldn't need to use MonadZero. I don't like fail either, but I must admit that I like allowing refutable patterns.
Thus, refutable pattern matches occuring in a do-expression would force the expression to be typed explicitly with MonadZero. This is probably a good thing, as it forces one to think about the consequences of not properly dealing with a pattern match in any monad which doesn't explicitly allow for failure.
Surely this is no different from not properly dealing with a pattern match in a case statement?
Even if this translation of do-syntax isn't accepted, I still think that we should have a separate MonadZero.
I agree. I miss MonadZero.
I'd also like to see the current use of MonadPlus split into MonadElse (or MonadOr) and MonadPlus, [...]
I agree, but I think such a proposal shouldn't be considered in isolation from standardising some MTL-like library.
Lastly, it would be nice to have some standard name for the function: option :: (MonadPlus m) => [a] -> m a option = foldr (mplus . return) mzero which seems to come up quite a bit in my experience with nondet monads.
I liked Oleg's suggestion: "choose", since it's the realisation of the axiom of choice. Though possible "mchoose" might be more appropriate. Even more appropriate might be to rationalise some of these naming conventions. Cheers, Andrew Bromage
On 04/01/06, ajb@spamcop.net <ajb@spamcop.net> wrote:
G'day Cale.
Quoting Cale Gibbard <cgibbard@gmail.com>:
I personally feel that the inclusion of 'fail' in the Monad class is an ugly solution to the problem of pattern matching, and gives the incorrect impression that monads should have some builtin notion of failure.
So do I.
We ought to be using MonadZero when we want to express failure, but it's gone!
I agree!
What do people think of the following proposal?
I don't like it.
My feeling is that do { p <- xs; return e } should behave identically (modulo the precise error message if the pattern match fails) to map (\p -> e) xs. Your proposal would make it into a map/filter hybrid.
Speaking more practically, if a pattern match is technically refutable, but I, as the programmer, intend it never to fail, then I shouldn't need to use MonadZero.
I don't like fail either, but I must admit that I like allowing refutable patterns.
Okay, so how about we separate these? do {p <- e; stmts} = e >>= \p -> do {stmts} do {p <-: e; stmts} = let ok p = do {stmts}; ok _ = mzero in e >>= ok (note the new symbol for monadically failing pattern match) This allows the two meanings to be clearly separated, and only the latter of the two will create a dependency on MonadZero, and it will do so consistently. - Cale
G'day all. I wrote:
My feeling is that do { p <- xs; return e } should behave identically (modulo the precise error message if the pattern match fails) to map (\p -> e) xs. Your proposal would make it into a map/filter hybrid.
Which, of course, it is now. I blame lack of caffeine. Quoting Cale Gibbard <cgibbard@gmail.com>:
Okay, so how about we separate these?
do {p <- e; stmts} = e >>= \p -> do {stmts} do {p <-: e; stmts} = let ok p = do {stmts}; ok _ = mzero in e >>= ok (note the new symbol for monadically failing pattern match)
Not happy. If anything, it should be the former which gets the new symbol, to make it the same as list comprehensions. But this would break backwards compatibility. Cheers, Andrew Bromage
independent of anything else, giving up error messages on pattern match failures in do notation is not acceptable. so, if the split were to happen, having two methods in MonadZero, one which takes a string argument, would be needed. John -- John Meacham - ⑆repetae.net⑆john⑈
On 05/01/06, John Meacham <john@repetae.net> wrote:
independent of anything else, giving up error messages on pattern match failures in do notation is not acceptable. so, if the split were to happen, having two methods in MonadZero, one which takes a string argument, would be needed.
John
Why does that have to be built into the class specification? Already, GHC can give line/column numbers for failed pattern matches in lambdas, it should be able to do the same for failed pattern matches in do-syntax in a similar way. I've never run into a case where I wanted the pattern match failure error message in the form of a String, wrapped in my monad datatype directly. If you were going to go that route, wouldn't a proper data type be preferred anyway, rather than a String? I can't actually do anything with that string other than to display it, since the report doesn't standardise its structure. Further, these cases seem sufficiently rare that if you're interested in catching information about where the pattern match failed, and using it in your program, you'd probably be better off actually handling failure directly, rather than relying on some system built into do-notation. Otherwise, what's wrong with just letting the thing throw an exception with a meaningful error message? The whole reason things switched to this from the way that they were in Haskell 98 is that apparently people found it confusing that refutable pattern matches in their do-blocks were causing these extra MonadZero typeclass constraints. That may be true, though I'm not sure it's really a language problem so much as a teaching problem. Further, pattern matches can switch from being irrefutable to refutable simply by modifing/extending a datatype, and some found it confusing that they now had a bunch of MonadZero errors cropping up after extending their type. Now, I think that this is a bit of a poor example. In reality, you probably want those error messages -- they serve as a major warning that your code is now probably incorrect, as it fails to deal with a potential case in your data type. Improve the error messages in the compiler if it's confusing. (People should be more aware that algebraic data types are not meant to be easily extensible. This is another good reason to want proper records, as they should take care of situations in which one expects data to be extended after the fact.) Does anyone have a real example of a nontrivial use of fail where the string argument was really important and which wouldn't be better served by MonadError, or by just throwing an exception? Usually when I define my monads, I try to ignore 'fail' as much as possible. In most cases, I just leave it completely undefined, as there's no sensible way to embed strings into my type, or even to fail properly at all. Personally, I'd be most happy with just going back to the Haskell 1.4 way of handling do-notation, and I see the reasons for adopting 'fail' in the first place as a bit specious. - Cale
Am Mittwoch, 4. Januar 2006 22:30 schrieb ajb@spamcop.net:
[...]
Though possible "mchoose" might be more appropriate.
These leading m's are not nice in my opinion.
Even more appropriate might be to rationalise some of these naming conventions.
Yes, we should remove those m's and use qualification where removal of those m's leads to name clashes.
Cheers, Andrew Bromage
Best wishes, Wolfgang
Am Mittwoch, 4. Januar 2006 21:54 schrieb Cale Gibbard:
I personally feel that the inclusion of 'fail' in the Monad class is an ugly solution to the problem of pattern matching, and gives the incorrect impression that monads should have some builtin notion of failure.
I totally agree!
[...]
Best wishes, Wolfgang
Hi folks Wolfgang Jeltsch wrote:
Am Mittwoch, 4. Januar 2006 21:54 schrieb Cale Gibbard:
I personally feel that the inclusion of 'fail' in the Monad class is an ugly solution to the problem of pattern matching, and gives the incorrect impression that monads should have some builtin notion of failure.
I totally agree!
I agree that 'fail' is a bit of a hack, but it has some sort of rationale to it. Have you forgotten that Haskell has a builtin notion of failure? If you choose to, you can see the 'fail' method simply as a way to deprecate this notion (in which well typed programs go with a bang) for a whimper-like notion of failure provided by the monad. The way it's used is consistent with that view: 'fail' gets called in situations which would make a pure [cough] computation bomb. I'm fairly grateful for it, to be honest. One thing which might help, if it were possible, is to allow subclasses to provide default implementations for the methods of superclasses. And then MonadZero (of which I'm broadly in favour, caveats another time) can provide a default implementation of fail. Also Monad can provide a default implementation of Applicative's methods; Applicative can provide a default implementation of fmap, etc. Whilst failure remains one of the basic freedoms, I think we should be ready to live with it. But yes, it is worth opening this can of worms again, I believe... All the best Conor
Cale Gibbard writes:
I personally feel that the inclusion of 'fail' in the Monad class is an ugly solution to the problem of pattern matching, and gives the incorrect impression that monads should have some builtin notion of failure. Indeed, it's becoming common to type the result of some function in an arbitrary monad in order to indicate the potential for failure, which is strictly speaking, not the right thing to do. (In a lot of cases, it's going to be no better than complete program failure)
We ought to be using MonadZero when we want to express failure, but it's gone!
Yeah, I don't like fail either. In fact, I usually forget to define it, even for instances of MonadPlus. There are typically three ways to indicate error in existing monad libraries, e.g., mzero :: MonadPlus m => m a fail :: Monad m => String -> m a throwError :: MonadError e m => e -> m a I would say that fail and throwError essentially have the same meaning, but I distinguish them from mzero. To my mind, 'mzero' means "no answer", whereas fail and throwError mean "something's wrong". For example, my implementation of Nondet doesn't backtrack over errors: mzero `mplus` m = m throwError e `mplus` m = throwError e Should a pattern match failure call mzero or throwError? I was originally going to say throwError, but now I'm not so sure. First, MonadError is severely non-H98 (fundeps). Second, we would either need the error type to belong to some class which includes pattern match failures, or have a dedicated throwPatternMatchFailure method in MonadError. Finally, you can write sensible code which backtracks on pattern-match failure, e.g., do ... Just a <- lookup ... ...
Even if this translation of do-syntax isn't accepted, I still think that we should have a separate MonadZero.
I like the idea of a separate MonadZero. Do we know why it was combined with MonadPlus? Were there efficiency concerns, or did people dislike having to declare all those separate instances?
I'd also like to see the current use of MonadPlus split into MonadElse (or MonadOr) and MonadPlus, as described at the bottom of http://www.haskell.org/hawiki/MonadPlus as it helps to clarify the distinction between backtracking-type failure and immediate failure in types. We could even put this distinction to good use in many monads which do support backtracking anyway:
instance MonadElse [] where [] `morelse` ys = ys (x:xs) `morelse` ys = (x:xs)
With backtracking monads, you can use Oleg's msplit operator to get morelse, soft-cut, and various other operations. class MonadPlus m => MonadChoice m where msplit :: m a -> m (Maybe (a, m a)) mif :: MonadSplit m => m a -> (a -> m b) -> m b -> m b mif p t e = msplit p >>= maybe e (\(x,xs) -> t x `mplus` (xs >>= t)) a `orElse` b = mif a return b With non-backtracking monads, you can use throwError or just use mplus and remind people that non-backtracking monads don't backtrack.
Lastly, it would be nice to have some standard name for the function: option :: (MonadPlus m) => [a] -> m a option = foldr (mplus . return) mzero which seems to come up quite a bit in my experience with nondet monads.
Mine too. Someone else mentioned "choose", which seems nice. Or, "fromList". Incidentally, would GHC optimize "msum (map return xs)" to "foldr (mplus . return) mzero xs"?
P.S. Oh, and don't get me started about the whole Functor subclass thing, and the inclusion of join in the Monad class. Of course I want those too. :)
For the recond, my ideal hierarchy would look something like this: class Functor f where map :: (a -> b) -> f a -> f b class Functor f => Applicative f where return :: a -> f a ap :: f (a -> b) -> f a -> f b lift2 :: (a -> b -> c) -> f a -> f b -> f c ap = lift2 ($) lift2 f a b = map f a `ap` b class Applicative m => Monad m where join :: m (m a) -> m a (>>=) :: m a -> (a -> m b) -> m b join m = m >>= id m >>= f = join (map f m) class Monad m => MonadZero m where nothing :: m a class MonadZero m => MonadPlus m where (++) :: m a -> m a -> m a class MonadPlus m => MonadChoice m where msplit :: m a -> m (Maybe (a, m a)) I guess you could put "return" in its own class, PointedFunctor, between Functor and Applicative, but I haven't seen a reason to. Even without that, it's probably excessive. -- David Menendez <zednenem@psualum.com> | "In this house, we obey the laws <http://www.eyrie.org/~zednenem> | of thermodynamics!"
participants (6)
-
ajb@spamcop.net -
Cale Gibbard -
Conor McBride -
David Menendez -
John Meacham -
Wolfgang Jeltsch