[Haskell-cafe] haskell programming guidelines

John Meacham john at repetae.net
Tue Feb 28 02:01:22 EST 2006


On Tue, Feb 28, 2006 at 01:09:03AM -0500, Cale Gibbard wrote:
> > Well, the benefit of the Identity monad is so that the user of a routine
> > can choose to recover gracefully by using a different monad, you only
> > use the Identity monad when you are making a choice to bottom out on
> > errors. using 'error' directly is not an option in said cases because it
> > would take away the ability of the user of a routine to catch errors
> > properly. error should only be used for reporting bugs that should never
> > happen, not user visible failure.
> 
> I'd argue that it would be better for the user to simply catch the
> value returned which indicates error explicitly, and throw the error
> themselves. This indicates that they have put thought into the fact
> that the function may fail.

so does using runIdentity, that is the point of it. You are saying I
want failure to bottom out, just like using it as a 'Maybe' means you
only care about whether it has a result or using it as a 'Either' means
you want the result string or using it as a WriterT Foo IO means you
want to possibly collect some results and have fail throw an IO
exception.

I consider it bad style to spend code on cases you never expect to
happen, if it takes too much work to write code that fails properly on
bugs, people arn't (and definitly should not have to) do the extra work,
they will just write code that fails poorly. Monadic failure is
absolutely great for writing robust, concise, code.

> > be handled, the user of it should.
> 
> Right, which is why minimal types for expressing the failure should be
> used, and the user should convert from those types to whatever larger
> environment they have in mind. If your function is simply partial, use
> Maybe, if you want to report error strings, use Either String. These
> types easily lift into any monad which support similar functionality.
> It also gives the users of your library more information about the
> exact way in which your functions may fail, just by looking at the
> type signatures, and gets them thinking about handling that failure.
> An arbitrary monad m doesn't indicate anything about the failure modes
> present.

ack! The user of a library is who should get to choose how to deal with
the error case, not the library writer.

I'd hate to give up such very common idioms as

-- collect error messages from all failing parsers 
[ err | Left err <- map parse xs] 

-- look up a string transformed by a map in another map, failing if it
-- is not in said other map.
runIdentity $ Map.lookup (concatMap (`Map.lookup` map) xs) smap

but the real power is when you combine monadic failure with combinators
and monad transformers

-- imagine some complicated function
f x xs = runWriterT $ mapM (\x -> foofunc x >>= tell) xs

the great thing about this is it is transparent to failure! so you can
build arbitrarily complicated transformers while still letting the user
of 'f' decide what to do with failure. this is a great feature, if
foofunc returned a data type, the writer of 'f' would be forced to deal
with failure, and might (likely will) do something silly like call
'error'. 

I really don't like it when things fail via 'error'. monadic failure
means they don't have to. not only can they let the user decide how
failure should be handled, but Monads provide exactly the compositional
tools needed to combine code in a such a way that preserves that
property.

imagine if Map.lookup returned Maybe Int, but writeInt returned (Either
String Foo).

now suddenly you couldn't do 
> Map.lookup x map >>= writeInt

By prematurely deciding on an algebraic type, you seriously limit the
usability of your code.

you say

"If your function is simply partial, use Maybe, if you want to report
error strings, use Either String."

which is exactly precicely what monadic failure lets you do. use the
routine in the way that makes sense. but more importantly it lets you write
monadic combinators that preserve said property.


> Well, that means that Reader, Writer and State, and any monad based
> upon them or their transformers does not have a meaningful fail. IO
> also does not have an interesting fail. It also means that all custom
> monads based on state transformers, say, don't have interesting fails.
> This is a very large chunk of the monads which people use in everyday
> code! The List monad and Maybe monad have nice fails, and that's why
> they should be in MonadZero.

IO definitly has an interesting fail, it throws a catchable IO
exception. (note, this is not the same as imprecise exceptions)

Reader,Writer, and State are stacked on top of Identity, which has error
as fail on purpose. if you don't like that you have the freedom to
either stack the transformer version on to another monad. Or there are
various transformers that give you an interesting 'fail' if you want it.
When you use Identity, you are saying 'error' is what you want. 


but in any case, you just stated the power of monadic fail right there.

"monads based on Reader, Writer, State won't have an interesting fail"
but you seem to miss the converse
"monads based on ones with interesting fails will have an interesting
fail"

but who determines what monad code runs in? the _user_ of the code. not
the code itself. if you want to handle failure, just use it in a monad
that has failure. it is completly up to the user of a routine how to
deal with failure and that is the great power of monadic failure and
typeclasses.


> I disagree that Identity, Reader, Writer, or State should be an
> instance of MonadError or MonadZero. They should simply not be used
> for that purpose. I'd like a monad hierarchy where if there is an
> instance of a class for a monad, then none of the methods of that
> class are identically bottom. It seems disingenuous to me to say that
> some type constructor implements certain functionality, and then
> implement it in a way which crashes the program. If you need failure
> in your monad, add it explicitly via a transformer, and if you use
> failure, you should express that via a class. Types and classes should
> be meaningful and informative about this sort of thing.


I really don't want programs to bottom out, which is why I like monadic
failure, it lets me write code that does not do so and use other peoples
code in such a way that it doesn't. bottom is bad! we should avoid it,
not encourage it! Monadic failure lets us avoid it in a very nice, clean
way. not having it would encourage people to write code that bottoms out
on failure with no good recovery path.


        John

-- 
John Meacham - ⑆repetae.net⑆john⑈


More information about the Haskell-Cafe mailing list