#2309: containers: specialize functions that fail in a Monad to Maybe

Dan Doel 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 
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.


More information about the Libraries mailing list