Three strategies for using custom Prelude

August 8, 2026 &english @code #haskell

There are several packages to replace haskell’s built-in Prelude module. Each has own reasoning and underlying philosophy. The point is – custom Preludes are a popular things. Lets discuss several ways to bring them into your project.

base without Prelude

Instead of depending on base package, you can simply replace it with base-noprelude in your package.yaml or project.cabal file. This package mimics the structure of corresponding version of base, but does not provide/exposr the Prelude module. This unlocks very simple thing trick – to create a Prelude module directly in your project.

module Prelude (module X) where

import Relude.Applicative as X
import Relude.Base as X
import Relude.Bool as X
import Relude.Container as X
import Relude.Debug as X
import Relude.File as X
import Relude.Foldable as X
import Relude.Function as X
import Relude.Functor as X
import Relude.List as X
import Relude.Monad as X
import Relude.Monoid as X
import Relude.Print as X
import Relude.String as X
import Relude.Extra.Map as X hiding (Key)

import UnliftIO.Async as X
import UnliftIO.Concurrent as X hiding (yield)
import UnliftIO.Directory as X
import UnliftIO.Environment as X hiding (getEnv)
import UnliftIO.Exception as X hiding (catchIO)
import UnliftIO.Foreign as X hiding (withArray)
import UnliftIO.IO as X
import UnliftIO.STM as X
import UnliftIO.Temporary as X

import Control.Lens.Getter as X
import Data.Generics.Labels ()

import Data.Aeson as X hiding (One)
import Data.Aeson.Text as X

import Network.HTTP.Client as X hiding (Proxy)
import Network.HTTP.Client.TLS as X

import System.FilePath as X hiding (isPathSeparator, pathSeparator)

import Conduit as X
import Data.Conduit.Combinators as X (iterM)
import Data.Conduit.List as X (chunksOf)
import Data.Conduit.TMChan as X

In regular haskell files of your project, you don’t need to change anything, it just works ©

Note, that base-noprelude package isn’t well-supported, so you might need to bring it in from some fork:

base-noprelude = hfinal.callCabal2nix "base-noprelude" (pkgs.fetchFromGitHub { owner = "Holmusk"; repo = "base-noprelude"; rev = "jan/base-4.21.0.0"; hash = "sha256-sooplvtRIy7BLrxXIFFD/GbQY63+Vg8McTNzd8XHH90="; }) { };

Mixins

Another fairly simple way to bring custom prelude is to use mixins feature from cabal (also well-supported with hpack):

dependencies:
  - name: base
    mixin:
      - hiding (Prelude)

Then you can either import some package (relude for example), which does provide custom Prelude module or define your own as before:

{-# LANGUAGE PatternSynonyms #-}

module Prelude (module X, jq) where

import Data.Map.NonEmpty as X (NEMap)
import Data.Scientific as X
import Data.Set.NonEmpty as X (NESet)
import Data.Text as X (pattern Empty, stripPrefix, dropAround)
import Data.Time.Clock as X
import Data.Time.Clock.POSIX as X
import Data.Time.Format.ISO8601 as X (iso8601Show)
import Path as X
import Relude.Applicative as X
import Relude.Base as X hiding (natVal, someNatVal)
import Relude.Bool as X
import Relude.Container as X
import Relude.Debug as X
import Relude.Enum as X
import Relude.Exception as X
import Relude.File as X
import Relude.Foldable as X
import Relude.Function as X
import Relude.Functor as X
import Relude.List as X
import Relude.Monad as X hiding (reader)
import Relude.Monoid as X
import Relude.Nub as X
import Relude.Numeric as X
import Relude.String as X
import Text.Time.Pretty as X

import Data.TypeLits as X hiding (Abs)
import Data.Reflection as X

import Relude.Extra.Map as X
import Relude.Extra.Tuple as X
import Relude.Extra.Type as X

import Data.Aeson.QQ as X
import Data.Aeson.Schema as X (schema, unwrap, toMap)
import Data.String.Interpolate as X
import Data.Time.Clock.Duration as X
import Data.Time.QQ as X
import Fmt as X
import Fmt.Terminal as X
import Text.URI as X
import Text.URI.QQ as X

import Text.Regex.Lens as X
import Text.Regex.Posix as X (Regex)
import Text.Regex.Quote as X

import Colog as X (log, logException, pattern D, pattern I, pattern E, pattern W)
import System.Environment as X
import UnliftIO as X (MonadUnliftIO, tryAny, throwIO, handleAny)
import UnliftIO.Exception as X (throwString)

import Control.Lens.At as X
import Control.Lens.Fold as X
import Control.Lens.Getter as X
import Control.Lens.Setter as X hiding ((.=), setting)
import Data.Generics.Labels ()

import Data.Aeson.Schema qualified
import Language.Haskell.TH.Quote qualified

jq :: Language.Haskell.TH.Quote.QuasiQuoter
jq = Data.Aeson.Schema.get

Custom project prelude

Nothing is wrong of course with just creating a custom Prelude for your project. First, I’d recommend to enable NoImplicitPrelude globally.

default-extensions:
  - NoImplicitPrelude

And then you are free to even have several Preludes to choose from:

  • Minimal Prelude, useful for low-level common modules
module Prelude.Minimal (module X) where

import Backends.Database as X

import Control.Lens as X (ASetter, at, filtered, folding, toListOf, (%=), (.=), (<>~))
import Control.Lens.Getter as X hiding (like)
import Data.Data as X (Data)
import Data.Data.Lens as X
import Data.Generics.Product as X hiding (IsList, field, param)
import Data.Generics.Wrapped as X
import Fmt as X hiding (format)
import GHC.TypeLits as X hiding (natVal, someNatVal)
import Prawda.Location as X

import DateRange as X
import JSON as X
import Time as X
import Val as X
  • Main Prelude, useful for high-level abstract modules, which usually include utility modules (which used Prelude.Minimal)
module Prelude.Main (module X) where

import Prelude.Minimal as X

import Vars.Types as X

import Config.TimeZoneConfig as X
import Programs as X
import PromisedLand as X
import Replays as X
import Utilities as X

Bonus: pre-commit hook

It might be useful to add git pre-commit hook to prevent developers from using stock Prelude by accident (in qualified or non-qualifies way).

no-prelude = {
  enable = true;
  entry = ''bash -c 'grep -E -n -H "^import Prelude($|\s)" "$@" && exit 1 || exit 0' --'';
  types = [ "haskell" ];
};