The Minimalist Prelude

…or why can’t Haskell be more like Purescript?

The Haskell language has a special module called “Prelude” that is imported by default even if you don’t explicitly import it. The Prelude module contains primitive types such as IO, Integer or Char as well as typeclasses such as Monadwhich are fundamental to the Haskell language. But then there’s also lots of other things exported by the Prelude which aren’t so much fundamental as they’re about providing you a convenient base vocabulary. Here’s a couple things exported by the Prelude I bet only very few use or even knew about:

asTypeOf :: a -> a -> a
lcm :: Integral a => a -> a -> a
gcd :: Integral a => a -> a -> a
lex :: ReadS String
subtract :: Num a => a -> a -> a
until :: (a -> Bool) -> (a -> a) -> a -> a
iterate :: (a -> a) -> a -> [a]

Some people would like to stuff even more into Haskell’s Prelude to the point you’d wonder if the term “prelude” would still be appropriate… but then again in music the term originally referred to a musical introduction to a much larger main course but over time preludes became complex standalone pieces in their own right. And so implementing the 73th alternate Prelude seems to have become a favorite pastime of many Haskellers right after they finished writing their Monad tutorial as the traditional rite of passage. However, I’d like to explore the opposite side of the spectrum: Why not try being minimalist? I mean, it’s the least we can do!


A surprising feature of Haskell is that by writing an explicit import of the special Prelude module the implicit import Prelude will be suppressed! You can even do the extreme of hiding all exports:

module M where
import Prelude ()
one :: Int
one = 1

and as you may expect this will rightly cause a compile failure:

M.hs:5:8: error:
Not in scope: type constructor or class ‘Int’
Perhaps you want to add ‘Int’ to the import list in the import of
‘Prelude’ (M.hs:3:1-17).
|
5 | one :: Int
| ^^^

There’s multiple ways to resolve this including following GHC’s advice and adding Int to the import by using import Prelude (Int). Or we could use qualified imports as from this point on the Prelude module behaves like any other module:

module M where
import qualified Prelude as Pre
one :: Pre.Int
one = 1 -- interestingly NOT `Pre.1`

With this in mind let’s see what a more minimalist Prelude could look like and impose as a general rule that we shall only bring into scope things that are magic or otherwise special to the language:

module Prelude.Minimalist (module Prelude) where
import Prelude
( -- The `Bool` type is wired into the if/then/else and
-- pattern guards syntactical elements
Bool(False, True),
      -- Primitive numeric types
Int, Integer, Float, Double, Rational, Word,
      -- We import the primitive typeclass names which are special
-- because they can be auto-derived for custom types
Eq, Ord, Enum, Bounded, Show, Read,
      -- These two are special because of number literals
Num, Fractional,
      -- These are special because of `do`-desugaring
Functor, Applicative, Monad, return,
      -- These are magic primitives
seq, error,
      -- The IO monad is special and it's required by the 
-- type-signature of `main :: IO ()`
IO
)

Some of the things that were intentionally left out:

  • () or [] or (,) or -> etc as these seem to be built-in syntax in GHC and you can’t control their scope
  • Other Numeric classes as they are not referenced by desugaring
  • otherwise because there’s nothing magic about it and it’s merely a redundant synonym of True: otherwise = True
  • Types such as Maybe or Either which are not wired into the language syntax
  • IO operations from System.IO

And now we can use the module above from another module like so

module MyMod where
import Prelude.Minimalist
import Prelude () -- suppress default Prelude
one :: Int
one = 1
pairM :: Monad m => m a -> m b -> m (a,b)
pairM ma mb = do
a <- ma
b <- mb
return (a,b)

And if you need to bring in more names from the Prelude module you can do so by simply importing them explicitly like so

import Prelude.Minimalist
import Prelude (Num(..), otherwise)

or you can use a qualified import

import Prelude.Minimalist
import qualified Prelude as Pre
two :: Integer
two = 1 Pre.+ 1

or you can combine both depending on your taste.


The goal of this exercise was to experiment with Haskell’s default Prelude import and trying to thin it down to a point where it is a bit more like Purescript’s default namespace while keeping in scope Haskell’s built-in names relevant to auto-deriving or syntax desugaring. I hope you enjoyed this short article and maybe it gave you some ideas for further experimentation…