A modern take on the Prelude
In the light of the ongoing discussions about the future of base
, I would like
to focus on a specific part of it: the Prelude.
TL;DR
I think most issues with Prelude could be fixed by:
- aknowledging that the content of Prelude should be context-dependent.
- improving the ergonomics of the use of alternative Preludes.
I expose my rough idea on how to improve the situation with a new language extension.
The polysemy of Prelude
From the section 5.6 “Standard Prelude” of the Haskell 2010 Report:
[Standard] Prelude and [standard] library modules differ from other modules in that their semantics (but not their implementation) are a fixed part of the Haskell language definition.
The Prelude module is imported automatically into all modules as if by the statement ‘import Prelude’, if and only if it is not imported with an explicit import declaration.
It is possible to construct and use a different module to serve in place of the Prelude. Other than the fact that it is implicitly imported, the Prelude is an ordinary Haskell module.
So there are two meanings for Prelude:
-
The standard Prelude is a module which exports a standard selection of definitions of the Haskell language.
Currently it is located in the package
base
and export much more than the Haskell 2010 report defines.While “standard” refers to the Haskell Report in this document, in the context of current
base
it means the living Haskell standard defined by the GHC implementation and its numerous extensions. It is illustrated by theGHC2021
extension vsHaskell2010
. -
The “Prelude“ feature is “one distinguished module, Prelude, which is imported into all modules by default” (see section 5.0 of the Haskell 2010 Report).
This is a feature to facilitate development: it aims to provide implicitly commonly used types, classes and functions.
While most programming languages have a kind of standard Prelude, the possibility to customize the Prelude feature is much more rare.
In the report the two meanings are merged and I think this is the root of much issues, presented in the two following sections.
Controversial standard definitions
Some standard definitions are controversial: e.g. the numeric hierarchy, String
, partial functions such as head and tail, lazy IO, etc. Changing them means creating a new standard that may introduce breaking changes in code using the standard Prelude.
The obvious solutions are to not use them or redefine them in another library. Not using them can be enforced e.g. with HLint/warnings, while there are multiple alternative Preludes with new definitions or export list.
Context-dependent commonly used definitions
The set of commonly used definitions is dependent of the context:
- Some definitions of the Prelude may not be used (e.g.
IO
in a pure code). - Some definitions of standard libraries are required but not exported by the standard Prelude: e.g. sized integral types such as
Int8
andWord64
; classes such asBits
orIx
;Char
predicates; functions such asmapMaybe
orsortBy
. - Some definitions are not part of the standard-by-the-report libraries:
Text
,Vector
,Map
,These
, etc.
“Commonly used” has no general definition, rather local ones. For example:
- In the context of the Haskell report and
base
, it is the result of what core Haskell developers agreed with the community. - In the context of a domain-specific library, it may include the low-level definitions in
GHC.Exts
required for the implementation. - In the context of an application using a library it may include its domain-specific definitions.
Prelude feature is neglected
There is no obvious solution for the issues presented above. Changes to the standard Prelude in base
have raised heated debates. Rightly so: Prelude
is usually imported in every Haskell file, so breakage is likely, thus conducting an impact assessment is not a trivial job. I would like to thank the CLC and people conducting such assessments!
Surprisingly, AFAIK improving the Prelude feature has drawn few attention, while in my opinion it is a solution for both ergonomics and stability:
- Better ergonomics: choose a Prelude adapted for each context.
- Improved Stability: avoid need for migration using a light Prelude or fix breakages in only one place.
Examples of current solutions
Without custom Prelude
It must rely on either:
- Explicit and detailed import of Prelude in every module. Does not seem a viable solution.
- Use
-XNoImplicitPrelude
and import explicitly from the original modules.
Using mixins in the .cabal
file
Compatible with GHC ≥ 8.2 and cabal-version ≥ 2.0.
Add the following in your .cabal
file:
mixins: base hiding (Prelude)
, my-prelude-library (My.Prelude as Prelude)
Using -XNoImplicitPrelude
Add the following in your .cabal
file:
default-extensions: NoImplicitPrelude
Then import a custom Prelude module such as My.Prelude
explicitly in every module.
Towards a new solution?
Among existing proposals that I am aware, the following one look quite interesting:
- Library-defined language extensions. Unfortunately it is dormant and its scope is much wider than the Prelude.
I dream of the following solution, which would facilitate the choice of the Prelude:
-
New GHC extension to set the Prelude module: e.g.
{-# LANGUAGE Prelude My.Module #-}
It seems better than using
-XNoImplicitPrelude
andimport My.Module
, because:- It identifies unambiguously where the Prelude is.
- It facilitates a migration to another Prelude: if set in the
.cabal
file, no import has to be added to the Haskell files. - Various Preludes can be used in the same (big) package in a clean way.
- It is one line less to write per file.
-
New field in
.cabal
files:Prelude: My.Prelude
, that would default to:Prelude: Prelude
.Cabal-install could automatically take care of incompatible GHC versions (i.e. not supporting the extension above) using implicit mixins.
-
New CLI option to use a specific Prelude in GHCi/cabal:
ghci --prelude=My.Prelude cabal repl --prelude=My.Prelude cabal init --prelude=My.Prelude
-
New config file option to set the default value in GHCi/cabal/stack.
Then GHC could ship alternative yet standard-ish Preludes, such as:
- Prelude: the current Prelude, maintained to ensure compatibility.
- Prelude.Safe: e.g. without
head
or with an alternative implementation, etc. - Prelude.Haskell2010: e.g. the exact Prelude of the Haskell 2010 report.
- Prelude.XXX: an hypothetical new Prelude that fixes the current flaws without backward compatibility. Notably it would be safer and exports
Text
,Int8
, etc. Cannot be part ofbase
, obviously.
I would like to know your opinion. Please correct me if there is any mistake, or prior art that I am not aware of! If there is sufficient interest I could open a GHC proposal.