Is using an alternative Prelude still recommended in 2022?

Hello!

I’m following along a book that’s a few years old and recommending this approach.

However, I find that I seem to get into trouble with some libraries (namely Aeson) expecting some types that clash with the alternative prelude I choose.

In other words, I find myself importing types from the standard prelude + from the alternative prelude library to satisfy the compiler.

So I’m thinking I could still be misunderstanding a few things regarding what types represent and how to satisfy the compiler. Or maybe it’s common practice for some libraries to expect to work with the standard prelude?

To be a little more specific, I stumble upon clashing definitions of what Text vs String vs [Char] mean in the context of my program vs the library I call to.

If you do use and recommend an alternative prelude, I’d be curious to hear which one you choose and why.

Thanks :slight_smile:

1 Like

A rule of thumb is “if you need to ask whether you need an alternative prelude, don’t use one”. They are primarily for production usage, and very poor for learning, since you end up with confusing name conflicts and trouble integrating with libraries (which almost exclusively use the default Prelude).

Alternative preludes have a lot of advantages, but normally they solve issues that arise in production codebases.

Many people (such as my work codebase) will cook their own Prelude with the re-exports they like and enable NoImplicitPrelude, importing their custom Prelude instead

When you stumble into Text vs String issues, be sure to add import Data.Text qualified as T to your file and then use T.pack :: String -> Text and `T.unpack :: Text → String".

11 Likes

Thanks. It sounds reasonable.

I want to build a web app. I’m not building anything mission critical, but I wouldn’t want to build something that performs badly on principle.

Once it’s built, how would I analyze the need to switch to a more performant prelude?

I suppose I would need to use a memory profiler such as this one to find the bottlenecks. Is that it?

Could you explain a little more what this means please? Does it mean that using a library may force me to isolate the code relying on the standard prelude, from other code relying on an alternative prelude?

Initially, I understood that using an alternative prelude could be a transparent process, but after seeing compile errors from Aeson complaining about the internal representations of certain types I have some doubts.

  1. Alternative preludes are maintained by a lot less people than ghc base
  2. ghc has breaking changes fairly frequently
  3. Alternative preludes’ maintainers are more likely to burn out and stop maintaining it, or lag behind ghc
  4. Library code should not introduce unnecessary dependencies for their users
  5. Any sufficiently large Haskell project develops their own prelude tailored to their task
  6. Any sufficiently small project doesn’t really need to worry about it

Alternative preludes are generally not a drop in replacement for prelude that boost the performance of your code, they usually expose certain standards they think are good and hide things they think are not good. So for example alt preludes might not expose String and expose Text instead, or change the type of head.

This can be a helpful way to learn what to use and what not to use (according to the alt preludes’ authors), but it does have a cost as well.

Also the ghc manual has a section on profiling
https://downloads.haskell.org/ghc/latest/docs/users_guide/profiling.html

5 Likes

Thanks @gilmi, that’s great feedback!

You convinced me, I’ll start to learn how to use the standard prelude properly first, then move from there.

If anybody wants to chime in with a differing opinion I’ll gladly listen to it of course :slight_smile:

1 Like

Just don’t use lazy IO from base. Instead use a streaming library. Unfortunately, many functions from base wrt file reading/writing do this.

1 Like

Hmm ok so after playing some more and taking a step back, I don’t know about the standard prelude after all…

I highly dislike that I can crash my program at runtime like this!

I don’t want to be able to call this kind of code. In fact I would want to avoid exceptions as much as possible and rather be forced to use a result type or something.

Prelude> read "1" :: Int
1
Prelude> read "" :: Int
*** Exception: Prelude.read: no parse

If I’m confused, please do tell me though.

1 Like

If you run hlint, it warns you about some bad behaved functions (like fromJust).

In general Prelude is not going away, partial functions are known, stay away from those when writing code.

1 Like

Stan will warn you about all partial functions from the prelude.

And in particular for read there is also readMaybe in base.

3 Likes

Thanks @jaror I’m going to look at Stan

For info @f-a, my vscode setup is not showing any warnings about potentially unsafe calls. Hlint does trigger though (blue squiggly)

image

Same story at the command line:

Many thanks. I was looking at the wrong page (GHC-Read) and missed it!

Thanks a ton @jaror!

Stan looks like a wonderful tool, you made my day :wink:

I feel better now, phew :sweat_smile:

4 Likes

If I’m reading this right, the main disadvantages to using an alternate prelude (that is lightweight enough to not add new types or classes or dependencies, is actively maintained, and that you agree with the philosophy of) are that it lags GHC a bit and it has more learning curve?

I ask because I’m in love with Relude and would be bummed to hear that in 2022 you should just use Prelude instead. As long as I’m understanding the disadvantages there correctly, that would be a worthwhile price for me.

1 Like

My biggest application uses Relude. When I’m organizing things into many small modules, I can’t get along without an alternative prelude, because the default just doesn’t have enough stuff in it. I default to putting Relude in most new projects. When I publish something as a library, I will often remove relude if I’m trying to minimize the dependency set.

As for name clashes - The longer I’ve worked with Haskell, the more I tend to use qualified imports. Typically for Aeson I’ll do something like

import qualified Data.Aeson as J
import qualified Data.Aeson.Key as J.Key
import qualified Data.Aeson.KeyMap as J.KeyMap
import Data.Aeson (ToJSON (toJSON))
2 Likes

I am really happy with relude at work: I find its philosophy agreeable, its utilities help a lot, its maintainers are reasonably responsive and work tends not to chase bleeding-edge GHC anyway. If I’m pushing a library to Hackage I will never use an alternate prelude because that’s unkind to my users. I don’t want them pulling in more dependencies than necessary (how many pet preludes could a large project transitively depend upon?).

1 Like

Forgot to reply to you…

Thanks. I’m not sure what this means concretly yet but I’ll try to keep this in mind :slight_smile:

Thanks for your feedback :slight_smile:

Yeah, I too enjoy explicitness, but mainly because I have trouble otherwise grokking where stuff comes from :grin:

Thanks for your feedback @jackdk. I suppose this is the key point.

I’m planning on working with just the standard prelude at first to get the feel for it, then I’ll reassess once I understand these trade-offs a bit better.

1 Like

Nice thread! It seems you’re at about the same point in your Haskell education as I, @benjamin-thomas. Here’s an exercise @chris-martin’s reply popped into my noggin.

Import your own FixPrelude.hs library of Prelude partial functions you’ve rewritten.

import qualfied FixPrelude as FP

Or, keeping in mind this has a potential to build bad habits, hide any Prelude functions you’ve rewritten.

import Prelude hiding ( read )
import FixPrelude

Many Prelude alternatives probably start this way, but it still seems to make sense as a personal learning exercise… especially when combined with Stan.

This exercise could also be a good companion to hacking at code with the NoImplicitPrelude GHC extension.

1 Like

Thanks @DeCentN2Madness,

This looks like a great exercise indeed and it make a lot of sens, I’ll try it!

1 Like

I am currently using the NoImplicitPrelude extension and thus regularly add explicit imports like this:

import Data.Bool (Bool (True))
import Data.Maybe (Maybe (Just, Nothing))
import Data.Eq (Eq (=))
-- ... much, much more imports

While that might seem masochistic at first, the reason is the Haskell Language Server and how it helps my IDE to efficiently manage my imports for me. My workflow is like this:

  • write a couple of lines of code without giving thought to imports
  • using my emacs shortcut for “lsp-execute-code-action” on every compiler error (expression underlined in red)
  • my IDE presents me with a list of choices, among those to add an import (e.g. Data.Eq (Eq))
  • run lsp-format-buffer to apply the brittany code formatter and have my imports look nice
  • after having worked out all the errors there is an IDE command to “remove all redundant imports” and I’m done

All my imports are explicit (and I use qualified imports whereever it helps readibility, this is well supported by HLS, too). Of course I can have all-explicit imports with Prelude, too. The above would simply looks like the follwing:

import Prelude (Bool (True), Maybe (Just, Nothing), Eq (=))

… but given how my IDE works, this is hardly more practical and arguably less readable.


There are advantages and disadvantages to my approach.

Advantages:

  • Being 100% explicit (as described above)
  • Routinely making an explicit choice what to import from where, e.g. id from Control.Category rather than from Data.Function, filter from Witherable or from Data.List, length from Data.Foldable rather than Data.List, …
  • There is a bit of an educational experience. You find <$>, fmap, and <&> in Data.Functor. You find pure, and <*> in Control.Applicative, you find $ and & in Data.Function, traverse_ in Data.Foldable, traverse in Data.Traversable and so on. There are reasons behind these modules that can be interesting especially when still learning.

Disadvantages:

  • Whenever I find myself adding those very basic imports, I wish for some sort of prelude that would take care of that—because even with my IDE this is repetitive work. (I then am reminded of the downsides of the existing Prelude—partial functions, and simply a lot of stuff that I don’t use like map, mapM, foldl—and I definitely don’t want to add my own, new, custom Prelude unless I have a very good reason.
  • The explicit imports like the ones above might confuse other programmers rather than help with readability: Am I hiding a custom reimplementation of Maybe within those imports? import Prelude hiding (Maybe) is much easier to understand
  • When I split up code of one module into two modules, I am refactoring code that works. In order to get the imports right, I copy all the imports over to the new module and then run “remove all redundant imports”. Adding all imports anew even with the help of my IDE would be cumbersome.
  • The import list gets even longer and takes up even more space on top of every module

Having read through this thread, I might give relude a try. I was reluctant to use anything else than Prelude because one of the added values of any sort of prelude would be that it’s fairly standard and makes my code faster to read rather than adding a dependency.