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.
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".
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.
Alternative preludes are maintained by a lot less people than ghc base
ghc has breaking changes fairly frequently
Alternative preludes’ maintainers are more likely to burn out and stop maintaining it, or lag behind ghc
Library code should not introduce unnecessary dependencies for their users
Any sufficiently large Haskell project develops their own prelude tailored to their task
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.
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 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.
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))
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?).
Yeah, I too enjoy explicitness, but mainly because I have trouble otherwise grokking where stuff comes from
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.
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.
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:
… 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.