#2309: containers: specialize functions that fail in a Monad to
dan.doel at gmail.com
Tue May 27 19:06:26 EDT 2008
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
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
> > [...]
> > 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. 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.
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
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.
More information about the Libraries