Before Haskell 98, there were Haskell 1.0 through 1.4. It's pretty interesting to see the development throughout the years, as features were added to the earliest versions of standardized Haskell.
For instance, the do-notation was first standardized by Haskell 1.3 (published 1996-05-01). In the Prelude
, we find the following definitions (page 87):
-- Monadic classes
class Functor f where
map :: (a -> b) -> f a -> f b
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
m >> k = m >>= \_ -> k
class (Monad m) => MonadZero m where
zero :: m a
class (MonadZero m) => MonadPlus m where
(++) :: m a -> m a -> m a
The same definitions are found in Haskell 1.4. I do have a few problems with this (e.g. the MonadPlus
reform hasn't happened here yet), but overall, it is a very nice definition.
This is very different from Haskell 98, where the following definition is found:
-- Monadic classes
class Functor f where
fmap :: (a -> b) -> f a -> f b
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
fail :: String -> m a
-- Minimal complete definition:
-- (>>=), return
m >> k = m >>= \_ -> k
fail s = error s
This is also the definition in Haskell 2010. I have the following problems with this definition:
MonadZero
andMonadPlus
are gone. They were useful classes.In case of a pattern match failure in a do-notation...
- Haskell 1.3 uses
zero
. The Left Zero law applies (zero >>= k = zero
), so you know what's supposed to happen. - Haskell 98 uses
fail msg
, wheremsg
is compiler-generated in case of GHC. Anything can happen, no guarantees about its semantics. Therefore, it's not much of a function for users. As a consequence, the behaviour of pattern match failures in Haskell 98's do-notation is unpredictable!
- Haskell 1.3 uses
Names are less general (e.g.
map
vs.fmap
). Not a big problem, but it's a thorn in my eye.
All in all, I think these changes weren't for the best. In fact, I think they were a step backwards from Haskell 1.4. Why were these things changed for Haskell 98, and why in this way?
As an aside, I can imagine the following defenses:
- "
fail
allows for locating errors." Only for programmers, and only at runtime. The (unportable!) error message is not exactly something you want to parse. If you really care about it, you should track it explicitly. We now haveControl.Failure
from thefailure
package, which does a much better job at this (failure x
behaves mostly likezero
). - "Having too many classes makes development and use too hard." Having too few classes breaks their laws, and those laws are just as important as types.
- "Instance-restricted functions are easier to learn." Then why isn't there a
SimplePrelude
instead, with most of the classes removed? It's only one magical declaration away for students, they can manage that much. (Perhaps{-# LANGUAGE RebindableSyntax #-}
is needed too, but again, students are very good at copy-pasting stuff.) - "Instance-restricted functions make errors more readable." I use
fmap
much more often thanmap
, so why notmap
andlistMap
instead?
map
got renamed tofmap
andmap
specialised to lists is that the error messages you get when mapping over lists are much more newbie-friendly that way. Imagine being new to Haskell (or programming in general) and just wanting to change values in a list and you get an error message aboutNo instance for (Functor f0)
! – kqrSimplePrelude
(havingmap = listMap
), more advanced users could opt to uselistMap
instead ofmap
. – user824425