ezyang’s blog

the arc of software bends towards understanding

Punt the Prelude

Conservation attention notice. What definitions from the Haskell 98 Prelude tend to get hidden? I informally take a go over the Prelude and mention some candidates.

(.) in the Prelude is function composition, that is, (b -> c) -> (a -> b) -> a -> c. But the denizens of #haskell know it can be much more than that: the function a -> b is really just the functor, so a more general type is Functor f => (b -> c) -> f b -> f c, i.e. fmap. Even more generally, (.) can indicate morphism composition, as it does in Control.Category.

all, and, any, concat, concatMap, elem, foldl, foldl1, foldr, foldr1, mapM_, maximum, minimum, or, product, sequence_. These are all functions that operate on lists, that easily generalize to the Foldable type class; just replace [a] with Foldable t => t a. They can be found in Data.Foldable.

mapM, sequence. These functions generalize to the Traversable type class. They can be found in Data.Traversable.

Any numeric function or type class. Thurston, Thielemann and Johansson wrote numeric-prelude, which dramatically reorganized the hierarchy of numeric classes and generally moved things much closer to their mathematical roots. While dubbed experimental, it's seen airplay in more mathematics oriented Haskell modules such as Yorgey's species package.

Any list function. Many data structures look and smell like lists, and support some set of the operations analogous to those on lists. Most modules rely on naming convention, and as a result, list-like constructs like vectors, streams, bytestrings and others ask you to import themselves qualified. However, there is Data.ListLike which attempts to encode similarities between these. Prelude.Listless offers a version of the Prelude minus list functions.

Monad, Functor. It is widely believed that Monad should probably be an instance of Applicative (and the category theorists might also have you insert Pointed functors in the hierarchy too.) The Other Prelude contains this other organization, although it is cumbersome to use in practice since the new class means most existing monad libraries are not usable.

repeat, until. There is an admittedly oddball generalization for these two functions in Control.Monad.HT. In particular, repeat generalizes the identity monad (explicit (un)wrapping necessary), and until generalizes the (->) a monad.

map. It's fmap for lists.

zip, zipWith, zipWith3, unzip. Conal's Data.Zip generalize zipping into the Zip type class.

IO. By far you'll see the most variation here, with a multitude of modules working on many different levels to give extra functionality. (Unfortunately, they're not really composable...)

  • System.IO.Encoding makes the IO functions encoding aware, and uses implicit parameters to allow for a "default encoding." Relatedly, System.UTF8IO exports functions just for UTF-8.
  • System.IO.Jail lets you force input-output to only take place on whitelisted directories and/or handles.
  • System.IO.Strict gives strict versions of IO functions, so you don't have to worry about running out of file handles.
  • System.Path.IO, while not quite IO per se, provides typesafe filename manipulation and IO functions to use those types accordingly.
  • System.IO.SaferFileHandles allows handles to be used with monadic regions, and parametrizes handles on the IO mode they were opened with. System.IO.ExplicitIOModes just handles IOMode.

7 Responses to “Punt the Prelude”

  1. Eric Mertens says:

    We need to nip this reverse generalization of (.) to fmap. It is no better than renaming (=<<) to concatMap!

  2. Imam Tashdid ul Alam says:

    I think you mean the type “(->) a” is the functor.

    Also, sometimes people need more specific types [citation needed?]. For example, I thought renaming fmap to map would be a good idea but then “map (1+) (return 2)” for example has type “(Num a, Monad f, Functor f) => f a” rather than the more obvious “[Int]”. Amazingly when I tried this in ghci the result was “3”. Not “[3]”. Admittedly, the confusion arises because when writing “map” I am still thinking “(a -> b) -> [a] -> [b]”, and wondering which Functor is ghci defaulting to. Interesting. Possibly potentially confusing. But to be honest I am fine with that.

    Nice post but we need to take this seriously. If only I could find the time (and the motivation) to push on the other prelude.

  3. wren ng thornton says:

    Indeed, the idea of using (.) for fmap needs to be nipped. The (.) operator is far more appropriate as a method of Category for composing morphisms. We already have (<$>) as a symbolic name for fmap, which is a far better name anyways since fmap is just generalizing ($).

    For generalized zipping, there are some other approaches too. There’s my version based on Chung-chieh Shan’s work with polyvariadic non-regular types. And there’s paczesiowa’s version which is vastly more complex but lets you avoid a few type signatures.

  4. C. McCann says:

    For what it’s worth–GHCi will default ambiguous Functor/Applicative/Monad values to IO, which shouldn’t be too surprising upon reflection. So “fmap (1+) (return 2)” at the prompt evaluates as the type “IO Integer”, which GHCi then binds and prints, as it does with any type “Show a => IO a”.

  5. Imam Tashdid ul Alam says:

    Aha! Thanks C. McCann.

  6. Imam, the “reader monad” in Control.Monad.Reader is isomorphic to (->) a. I’ve updated it to be the less ambiguous notion though.

  7. jberryman says:

    That was quite an interesting roundup. Thanks! I stumbled upon the Numeric Prelude when I was writing a blog post about lazy arithmetic:


    …but I wasn’t familiar with most of these other ideas and projects.

Leave a Comment