
On Tuesday 27 May 2008, Ross Paterson wrote:
On Sun, May 25, 2008 at 07:58:44AM -0400, Dan Doel wrote:
I don't think we should get rid of fail. There's nothing wrong with it, per se. It just shouldn't be in Monad, it should be in MonadPlus (or MonadZero if things get split back up 1.4 style). fail is mzero that takes a string to explain what happened.
No, it's a different value: one can write programs that distinguish them. Haskell 98 treats error "string" as _|_ only because it can't analyse _|_.
fail is not identical to error. That is merely the default implementation. For instance: fail s = Nothing :: Maybe a fail s = [] :: [a] fail s = Left s :: Either String a In particular, a monad that is an instance of MonadPlus should have an implementation of fail that is a proper value in that monad, and not just bottom. If not, I'd wager it's a bug in the Monad instance. If fail were moved to MonadPlus(Zero), we could have: class Monad m => MonadPlus m where ... fail s = mzero mzero = fail "mzero" And the only way to even accidentally define fail as bottom would be to define neither fail nor mzero.
[...] The problem is calling fail in monads that don't have an mzero, because they don't have a notion of failure.
That is indeed part of the problem, because someone will define an instance for those monads, for the sake of convenience.
Define an instance of what? If someone uses error to define an instance of MonadZero to get fail in a monad that shouldn't have it, that's their problem. It doesn't mean that using a monads-with-proper-fail class in the standard library is flawed.
There is a choice here between convenience and safety. What would Haskell choose?
Monads that are instances of MonadPlus are (or ought to be) precisely the ones in which it's safe to call fail, so how are we giving up safety with the more general solution (the only exception I can think of is IO, where fail/mzero throws an IO exception, and people think that this isn't exceptional enough to warrant that, but I think that's more of a case against IO being MonadPlus than against using MonadPlus + fail in lookup/readM/...).
[...] That's not to mention potential information loss compared to fail.
which does not arise in this particular case.
No, but it does arise with readM, but proper use of fail got shouted down there, too.
[...] I just think it's frustrating that we have abstractions that do exactly what we want, and then don't use them. :)
It's not an abstraction: it's overloading. It conflates three different things (an element of a view, exceptions and runtime errors).
How is it not abstraction? Elsewhere, Isaac Dupree said that for proper implementation of lookup, we want two operations: unit :: a -> m a zero :: m a unit injects a proper value into a possibly-failing computation, and zero expresses failure. But, as we know, using only those, and pattern matching, we end up with code like this: case lookup k m of Nothing -> Nothing -- overall computation is a failure Just a -> case lookup k' m of Nothing -> Nothing -- same as above Just b -> ... which is annoying. So, in general, we want one more sort of operation, for chaining together computations in a way that propagates failure: (>>=) :: m a -> (a -> m b) -> m b zero >>= f = zero -- (and m >>= const zero = zero, but IO fails there) Maybe is a concrete type (constructor) that provides all this, but the general name for such things is, precisely, MonadZero[1]. Defining lookup and other fallible operations in terms of a general class of things that properly support failure instead of one particular one sounds like abstraction to me, but I'd settle for just calling it 'useful'. :) And although Maybe a provides the above operations, it doesn't strike me as the most commonly used type to do so. Specializing to Maybe means it doesn't automatically work in any of [], Either e, MaybeT m or FooT all-of-the-above. At best, you'll have to manually annotate with lifts everywhere. It's the exact same issue we currently have with transformers over IO, where one has to place liftIO all over such code. But, this is, of course, all merely my opinion. -- Dan 1: One could, of course, decide to go with some kind of ApplicativeZero, or so on. Or even PointedFunctorZero, because fmap is a generic way to use a possibly-failing computation with a pure function with automatic failure propagation. Strictly speaking, the composition/chaining operation isn't needed by lookup/etc., and Isaac's point is valid, but I don't think MonadZero or something like it is too overly specific. Perhaps with class aliases we could hope for some kind of pointed pseudo-functor with zero that isn't too arduous to use, but we don't have those currently.