The Haskell Prelude is the default import into all Haskell modules, it provides an endless number of ways to shoot ourselves in the foot and historical cruft that can’t be removed. While it is difficult to fix upstream, we can however remove the Prelude entirely on a project-level and replace it with a more sensible set of defaults using the -XNoImplicitPrelude
language extension.
There are two philosophies on building new Preludes:
- Big Vehicle Prelude – Fix most of the deficiencies in the Prelude by introducing new abstractions that replace large portions of the basic types and class, and generally redefine the way we write Haskell. See numeric-prelude .
- Small Vehicle Prelude – Fix the broken parts of the Prelude by building on existing types and classes and masking broken bits. See basic-prelude .
I don’t prescribe to the large vehicle approach, a lot of the default Prelude is not ideal but good enough to get the job done. And interoperability with the rest of the ecosystem, which typically uses default Base and Prelude, is incredibly important.
Inprevious posts I’ve written about how rolling our own small-vehicle Prelude is a good idea for large teams working in industry but I left it somewhat ambiguous about what to include. Obviously everyone’s company is different so I thought I’d expand on what I consider a sensible set of defaults that one could use to then build a custom Prelude. We’ll call this a Protolude .
Example Project
Module Structure
The basic module structure is consists of a main module reexport which we’ll dump everything in called X
. And various miscellaneous functions which we’ll reexport from the root module. Off of this we’ll have several utilities modules which have domain specific functions.
{-# LANGUAGE NoImplicitPrelude #-} module Protolude ( module X, identity, bool, (&), uncons, applyN, print, LText, LByteString, ) where import qualified Prelude as P import qualified List as X import qualified Show as X import qualified Bool as X import qualified Debug as X import qualified Monad as X import qualified Applicative as X
Base Types
The Basic types ( Int
, Integer
, Float
, Char
) we reexport wholesale along with their associated functions.
-- Base types import Data.Int as X import Data.Bits as X import Data.Word as X import Data.Bool as X hiding (bool) import Data.Char as X (Char) import Data.Maybe as X hiding (fromJust) import Data.Either as X import Data.Complex as X import Data.Function as X ( id , const , (.) , flip , fix , on )
We define the flipped application operator for GHC < 7.10 which does not provide this be default.
(&) :: a -> (a -> b) -> b x & f = f x
We rename the id
function to identity since it’s far more likely we’ll have variable names called id
over the identity function. This is a contentious point and your mileage may vary with this change.
identity :: a -> a identity x = x
The bool function (the combinator analogue of maybe
and either
) is provided by GHC 7.10, but we reexport it for earlier versions.
bool :: a -> a -> Bool -> a bool f t b = if b then t else f
The uncons
function is not provided by default, but it unpacks a list into a Maybe tuple containing the head in the first tuple parameter if it exists.
uncons :: [a] -> Maybe (a, [a]) uncons [] = Nothing uncons (x:xs) = Just (x, xs)
The applyN
function takes a function and a count and applies it n
number of times.
applyN :: Int -> (a -> a) -> a -> a applyN n f = X.foldr (.) id (X.replicate n f)
And then we reexport various boolean combinators for working with branching monadic logic.
whenM :: Monad m => m Bool -> m () -> m () whenM p m = p >>= flip when m unlessM :: Monad m => m Bool -> m () -> m () unlessM p m = p >>= flip unless m ifM :: Monad m => m Bool -> m a -> m a -> m a ifM p x y = p >>= /b -> if b then x else y guardM :: MonadPlus m => m Bool -> m () guardM f = guard =<< f
Safe
Safe provides Maybe versions of many of the various partial functions ( head
, tail
) that are shipped by default. Wrapping it up in a Maybe is widely considered the right approach and if Haskell were designed today, they would not be present.
-- Maybe'ized version of partial functions import Safe as X ( headMay, initMay, tailMay )
Debugging
Various debugging and trap commands are still provided since they are useful for partial code and various fatal program logic. However now they have a warning emitted by the compiler if they are left in place, with position information about where they are used. This can be toggled between production code and debugging code.
{-# WARNING undefined "'undefined' remains in code" #-} undefined :: a undefined = P.undefined {-# WARNING error "'error' remains in code" #-} error :: P.String -> a error = P.error {-# WARNING trace "'trace' remains in code" #-} trace :: P.String -> a -> a trace = T.trace {-# WARNING traceShow "'traceShow' remains in code" #-} traceShow :: P.Show a => a -> a traceShow a = T.trace (P.show a) a {-# WARNING traceShowM "'traceShowM' remains in code" #-} traceShowM :: (P.Show a, P.Monad m) => a -> m () traceShowM a = T.traceM (P.show a) {-# WARNING traceM "'traceM' remains in code" #-} traceM :: P.Monad m => P.String -> m () traceM = T.traceM {-# WARNING traceIO "'traceIO' remains in code" #-} traceIO :: P.String -> P.IO () traceIO = T.traceIO {-# WARNING notImplemented "'notImplemented' remains in code" #-} notImplemented :: a notImplemented = P.error "Not implemented"
Either
Various either combinators to convert between Maybe and Either probably should be provided but are not, there are four various combinations of these. Handling a maybe which happens to have a monoidal instance for mempty in Left is also fairly common task.
leftToMaybe :: Either l r -> Maybe l leftToMaybe = either Just (const Nothing) rightToMaybe :: Either l r -> Maybe r rightToMaybe = either (const Nothing) Just maybeToRight :: l -> Maybe r -> Either l r maybeToRight l = maybe (Left l) Right maybeToLeft :: r -> Maybe l -> Either l r maybeToLeft r = maybe (Right r) Left maybeToEither :: Monoid b => Maybe a -> Either b a maybeToEither = maybe mempty
List
The various list functions are exported wholesale excpet for the ones which are generalized by Foldable and Traversable. head is reexported as a total function written in terms of foldr. The ordNub
function is accidentally quadratic in the Prelude is replaced by a logarithmic variant using a hashmap underneath.
head :: (Foldable f) => f a -> Maybe a head = foldr (/x _ -> return x) Nothing sortOn :: (Ord o) => (a -> o) -> [a] -> [a] sortOn = sortBy . comparing -- O(n * log n) ordNub :: (Ord a) => [a] -> [a] ordNub l = go Set.empty l where go _ [] = [] go s (x:xs) = if x `Set.member` s then go s xs else x : go (Set.insert x s) xs
Monad
The core moand class, functions, and combinators are reexported wholesale. Variants like mapM
are provided by Data.Traversable
instead of Control.Monad
.
module Monad ( Monad(..) , MonadPlus(..) , (=<<) , (>=>) , (<=<) , forever , join , mfilter , filterM , mapAndUnzipM , zipWithM , zipWithM_ , foldM , foldM_ , replicateM , replicateM_ , concatMapM , guard , when , unless , liftM , liftM2 , liftM3 , liftM4 , liftM5 , liftM' , liftM2' , ap , (<$!>) ) where import Prelude (concat, seq) import Control.Monad concatMapM :: (Monad m) => (a -> m [b]) -> [a] -> m [b] concatMapM f xs = liftM concat (mapM f xs)
Strict versions of liftM
are also provided by default since this is a common source of unexpected laziness.
liftM' :: Monad m => (a -> b) -> m a -> m b liftM' = (<$!>) {-# INLINE liftM' #-} liftM2' :: (Monad m) => (a -> b -> c) -> m a -> m b -> m c liftM2' f a b = do x <- a y <- b let z = f x y z `seq` return z {-# INLINE liftM2' #-}
Applicative
The applicative module core types are reexported wholesale. Unlike in 7.8 we have the Applicative class in scope by default, as it should be in this day and age.
-- Applicatives import Control.Applicative as X ( Applicative(..) , Alternative(..) , Const(..) , ZipList(..) , (<**>) , liftA , liftA2 , liftA3 , optional )
Typeclasses
The core GHC typeclasses for ordering and numeric tower are reexported wholesale. We also bring Traversable and Foldable into scope masking a few of the partial functions which generally should be avoided. Semiring is also common enough these days that it should be in scope implicitly.
-- Base typeclasses import Data.Eq as X import Data.Ord as X import Data.Monoid as X import Data.Traversable as X import Data.Foldable as X hiding ( foldr1 , foldl1 , maximum , maximumBy , minimum , minimumBy ) import Data.Semiring as X import Data.Functor.Identity as X import Data.Functor as X ( Functor(..) , ($>) , (<$>) , void )
Deepseq
Deepseq is usually an important typeclass to derive for various usecases, and so we bring in the deepseq
library and it’s various functions.
-- Deepseq import Control.DeepSeq as X ( NFData(..) , ($!!) , deepseq , force )
Data Structures
The core data structures provided by GHC are reexported, but with List we make sure to mask the partial functions, and operators are provided in more general interfaces.
-- Data structures import Data.Tuple as X import Data.List as X ( splitAt , break , intercalate , isPrefixOf , drop , filter , reverse , replicate )
The containers library all provide functions which overlap with each other (lookup, insert, etc) so we just export the types for these structures so we can use them in signatures. On a module level these would then be typically imported using a qualified import like import qualified Data.Map as Map
.
import Data.Map as X (Map) import Data.Set as X (Set) import Data.Sequence as X (Seq) import Data.IntMap as X (IntMap) import Data.IntSet as X (IntSet)
Monad Transformers
The basic mtl
transformer stack are usually ubiqitious in modern Haskell, specifically StateT, ReaderT and ExceptT. We don’t export all functions in these modules but just enough that most uses cases of the common transformers are brought into scope.
-- Monad transformers import Control.Monad.State as X ( MonadState, State, StateT, put, get, gets, modify, withState, runStateT, execStateT, evalStateT, ) import Control.Monad.Reader as X ( MonadReader, Reader, ReaderT, ask, asks, local, runReader, runReaderT, ) import Control.Monad.Except as X ( MonadError, Except, ExceptT, throwError, catchError, runExcept, runExceptT, ) import Control.Monad.Trans as X ( MonadIO, lift, liftIO, )
Wired-In Types
Several basic types (IO, Show) are necessary to do anything so we obviously bring these in. The Exts
provides the various pointer types for working with FFI as well as the Constraint type which is somewhat common in the presence of -XConstraintKinds
and more advanced type family techniques.
-- Base GHC types import GHC.IO as X (IO) import GHC.Num as X import GHC.Real as X import GHC.Float as X import GHC.Show as X import GHC.Exts as X ( Constraint , Ptr , FunPtr , the )
Generics
Generics are also ubiqitious in modern Haskell and the various typeclasses and type synonyms should be provided so that we can -XDeriveGeneric
as well as implement default instances for Generic classes.
-- Generics import GHC.Generics ( Generic(..) , Rep , K1(..) , M1(..) , U1(..) , V1 , D1 , C1 , S1 , (:+:) , (:*:) , NoSelector , Rec0 , Par0 , Constructor(..) , Selector(..) , Arity(..) , Fixity(..) )
String Types
Strings in Haskell are a giant fractal of suffering. In a just world, we would all just use Text most of the time, and ByteString when we needed to deal with network but we don’t live in that world an large portions of Hasell ecosystem still use the wildly-inefficient linked-list of Char.
Controversially we just reexport the Lazy text type as a type synonym LText
and provide the string-conv
library function toS
which automatically converts between any two string types using a multiparam typeclass. String heavy programs might also consider reexporting the various Text and ByteString builder classes.
-- ByteString import qualified Data.ByteString.Lazy import qualified Data.ByteString as X (ByteString) -- Text import Data.Text as X (Text) import qualified Data.Text.Lazy import qualified Data.Text.IO import Text.Printf as X (printf) import Data.Text.Lazy ( toStrict , fromStrict ) import Data.String.Conv as X ( strConv , toS , toSL , Leniency(..) ) -- Printf import Text.Printf as Exports ( PrintfArg , printf , hPrintf )
type LText = Data.Text.Lazy.Text type LByteString = Data.ByteString.Lazy.ByteString
IO
IO is essentially obvious, so we reexport the various IO operations and command low-level command line option and file handler functions.
-- IO import System.Exit as X import System.Environment as X (getArgs) import System.IO as X ( Handle , hClose )
In a just world we’d have a generic putStr
that didn’t require us to put boilerplate imports in every damn module just to print a string, but alas we don’t live in that world. But we can implement a basic class to do this for the common text representation and then just export the class instead of the four libraries that all provide the same interface.
class Print a where putStr :: MonadIO m => a -> m () putStrLn :: MonadIO m => a -> m () instance Print T.Text where putStr = liftIO . T.putStr putStrLn = liftIO . T.putStrLn instance Print TL.Text where putStr = liftIO . TL.putStr putStrLn = liftIO . TL.putStrLn instance Print BS.ByteString where putStr = liftIO . BS.putStr putStrLn = liftIO . BS.putStrLn instance Print BL.ByteString where putStr = liftIO . BL.putStr putStrLn = liftIO . BL.putStrLn instance Print [Char] where putStr = liftIO . Prelude.putStr putStrLn = liftIO . Prelude.putStrLn -- For forcing type inference putText :: MonadIO m => T.Text -> m () putText = putStrLn {-# SPECIALIZE putText :: T.Text -> IO () #-} putLText :: MonadIO m => TL.Text -> m () putLText = putStrLn {-# SPECIALIZE putLText :: TL.Text -> IO () #-}
Like the functions above we automatically lift print and the various string IO operations into a generic MonadIO
instance so we can embed them in transformer stacks.
print :: (X.MonadIO m, P.Show a) => a -> m () print = liftIO . P.print
Concurrency
The basic thread primitives are common enough that they should be brought into scope implicitly. The async
library provides the preferred method of interacting with concurrent logic and so we export this wholesale as well.
-- ST import Control.Monad.ST as ST -- Concurrency and Parallelism import Control.Exception as X import Control.Concurrent as X import Control.Concurrent.Async as X
Now in our cabal file we can just add:
default-extensions: NoImplicitPrelude
And then include your sensible Prelude in your modules. You can escape hatch out of this choice on a module-by-module basis by just pulling in Prelude explicitly using import Prelude
.
So that’s the basic proto-prelude I use for my large multi-cabal-file projects. Your mileage by vary on some of these choices, but I consider this a pretty sensible and practical foundational set of functions and types that should at least provide a good starting point.
转载本站任何文章请注明：转载至神刀安全网，谢谢神刀安全网 » Building a Better Custom Haskell Prelude