A modern take on the Prelude

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 the GHC2021 extension vs Haskell2010.

  • 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 and Word64; classes such as Bits or Ix; Char predicates; functions such as mapMaybe or sortBy.
  • 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:

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 and import My.Module, because:

    1. It identifies unambiguously where the Prelude is.
    2. It facilitates a migration to another Prelude: if set in the .cabal file, no import has to be added to the Haskell files.
    3. Various Preludes can be used in the same (big) package in a clean way.
    4. 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 of base, 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.

7 Likes

Looks good!

I think we could easily have -prelude-is just like we have -main-is. Then we could use {-# OPTIONS_GHC -prelude-is=My.Module#-} for now until some support is added to Cabal.

2 Likes

Ok done:

4 Likes

@hsyl20 I’m not sure this warrants a new flag: base-noprelude + Cabal mixins to rename any module to Prelude should do the trick.

2 Likes

@hsyl20 thank you, that was fast! I like the name “prelude-is”. If your MR is accepted, I think it would be good to propose a specific syntax via a language extension, e.g. {-# LANGUAGE PreludeIs My.Module #-}. It could be done in a further release, if a period of experimentation is required. As for me I would like it as soon as possible. How can I help you?

I dislike that syntax, since no other language pragma takes an argument. Perhaps a better alternative would be a new field in the package description, since this feature is most useful at package scale.

Can you argue why the current solutions are not good enough? As you mention and @bodigrim repeats, we already have mixins which can do this. Are mixins really so bad that we need to introduce a whole new GHC option and Cabal field for one of their use cases?

2 Likes

The last time I tried this it really confused Haskell Language Server. I otherwise liked the approach (though it was a bit tedious setting up for each of our components, it’s mostly a one-off cost). I should revisit that and file some issues!

2 Likes

@Bodigrim base-prelude seems to be abandoned. Cabal mixins could be used but I like the option of having this directly in GHC. It seems more principled to me. At some point we could have NoImplicitPrelude the default and Cabal always explicitly specifying -prelude-is. This would uncouple ghc and base more :slight_smile:

@Wismill specific syntax is always more difficult to get consensus on. {-# LANGUAGE PreludeIs My.Module #-} would be the first language pragma to take an argument as @bradrn wrote. Note that with the flag we will have {-# OPTIONS_GHC -prelude-is My.Module #-} which is quite similar, so special syntax won’t buy us much. Similarly in Cabal, we can use ghc-options: -prelude-is My.Module without special syntax for now.

How can I help you?

I don’t think adding the flag requires a GHC proposal, but opinions may differ. If it’s deemed necessary, you can help by writing the proposal :slight_smile:

specific syntax is always more difficult to get consensus on. {-# LANGUAGE PreludeIs My.Module #-} would be the first language pragma to take an argument as @bradrn wrote.

OK, I did not know it would be an issue. {-# OPTIONS_GHC -prelude-is My.Module #-} is fine, but it is not very elegant, is it?

Can you argue why the current solutions are not good enough? As you mention and @bodigrim repeats, we already have mixins which can do this. Are mixins really so bad that we need to introduce a whole new GHC option and Cabal field for one of their use cases?

@jaror @bodigrim Mixins are a good feature, but I would like to make the Prelude a first class feature. I think nothing beats prelude-is in term of readability: if I open a Cabal file, I could see easily that a custom prelude is used [^1], even if I did not know about this field. Whereas with mixins you need to known how they work, then understands that the boilerplate only mean using a custom Prelude. Also, how would you specify it when calling ghci or cabal repl outside a project? What about the support of mixins in stack?

[^1]: Recommend put it in the first fields.

If we agree using custom Prelude is a good practice, then the main obstacle to a wider adoption is its ergonomics, because the technical solutions already exist.

@hsyl20 has already done a great job in starting a GHC MR. Implementing a Cabal field would be nice; I would volunteer to do it if this thread shows interest to it. In fact the GHC flag and Cabal field are independent features, as the Cabal field can be implemented solely with (implicit) mixins. Using a GHC flag is a better option though, because it gives more flexibility (e.g. using multiple custom preludes in a lib) and make the Prelude feature really first class in GHC.

1 Like

I don’t believe in a better Prelude that existing one for this reason. However I do use (local) custom prelude per project (.i.e I write my own prelude which could reexport part of a alternative prelude), so there is indeed a rationale for having a better to way to change the implicit prelude.

At the moment it is two lines in a file , the pragma with NoImplicitPrelude and the import the desired prelude. If the point of a new pragma is only to write the two line in one I am not sure there is much benefit.

However, If I can write it ONCE for the all project then I’m all for it. So having a prelude-is in the cabal file which set automatically NoImplicitPrelude and import the new prelude in every file is a good idea (as long as it allows local prelude.

4 Likes

There is a simpler way to obtain a per-project Prelude today, with no per-module cost whatsoever. Note that the Haskell report quoted above says nothing about where the Prelude module is obtained? I just ran an experiment:

$ cat Prelude.hs 
{-# Language NoImplicitPrelude, PackageImports #-}
module Prelude where
import "base" Prelude (IO, putStrLn)
hello :: IO ()
hello = putStrLn "Hello World!"
$ cat PreludeTest.hs 
main = hello
$ ghc PreludeTest.hs
[1 of 3] Compiling Prelude          ( Prelude.hs, Prelude.o )
[2 of 3] Compiling Main             ( PreludeTest.hs, PreludeTest.o )
[3 of 3] Linking PreludeTest
[mario@fedora tmp]$ ./PreludeTest 
Hello World!

[EDIT] I just tested this setup with Cabal, and it works as well. All you need to do is add other-modules: Prelude stanza. There is something funky about GHC though. When I tried runhaskell instead of ghc I got an error:

$ runhaskell PreludeTest.hs 

<interactive>:1:1: error:
    attempting to use module ‘main:Prelude’ (./Prelude.hs) which is not loaded

I guess I’ll report this bug.

[EDIT] The bug is known.

@blamario I know the trick, but you find its pitfall. See this containers issue. That is why I did not propose this solution.

The pitfall is clearly a GHCi bug. The correct response is to report and fix the bug, not to work around it.

1 Like

@blamario I think the issue is already reported. I do not see the point to propose a solution that does not work properly with a current release of GHC. It seems better to use a workaround while the issue is fixed and backported. The mixins is a much better option.

1 Like

@Wismill We discussed this quickly with the GHC team today. The conclusion was that it would useful to write a GHC proposal to get more feedback on the feature and on unintended consequences (in HLS, etc.).

According to the doc you can have your custom prelude by just having a Prelude.hs file.

If this is true using a pre made custom prelude is just a matter of having

module Prelude (export X)
where
import YourFavoritePrelude as X

in your project. Unless I am missing something that solves everything doesn’t ? No need to new extension, modify the cabal file etc …

We already discussed this above.

Sorry, didn’t see it. Thanks

This thread made me realize we need at least a single popular signature and implementation that forces everybody to know how use mixins and backpack.