# Haskell Patterns and Best Practices This document contains common Haskell patterns and best practices for efficient and idiomatic Haskell development. These patterns are particularly useful for human-AI collaboration, where clear communication and code understanding are essential. ## Reflections on the Clod Project Working on Clod provided valuable insights into developing Haskell applications collaboratively between humans and AI. Some lessons we learned: ### What Worked Well 1. **Capability-based security**: Using explicit capability tokens (`FileReadCap`, `FileWriteCap`) proved to be an elegant approach for enforcing access restrictions. The type system effectively prevented accidental security violations. 2. **Newtype wrappers**: Wrapping primitive types (like `String`) in newtypes (`IgnorePattern`, `OptimizedName`, `Checksum`) helped prevent confusion and enforce type safety throughout the application. 3. **Clear monad stack**: The `ClodM` monad combined `ReaderT` and `ExceptT` to provide a clean approach to dependency injection and error handling. This simplified the application compared to a more complex effects system. 4. **Modular architecture**: Breaking functionality into focused modules with clear responsibilities made the codebase easier to extend and maintain. ### Challenges Encountered 1. **Advanced type-level programming**: While the `AdvancedCapability` module showcases sophisticated type-level techniques, there's a point of diminishing returns where complexity can outweigh benefits in a pragmatic application. 2. **Effects system complexity**: Our early attempts at using more advanced effects systems (like Polysemy) introduced additional complexity without proportional benefits for this specific use case. 3. **Testing stateful code**: Testing code that interacts with the filesystem required careful setup and teardown of test environments. 4. **Cross-platform considerations**: Ensuring consistent behavior across operating systems required attention to path normalization and platform-specific conventions. ## Functional Programming Patterns ### Pure Functions and Side Effects - **Pure Functions**: Prefer pure functions over impure operations whenever possible. Pure functions are easier to reason about, test, and compose. ```haskell -- Pure function calculateTotal :: [Item] -> Price calculateTotal items = sum (map itemPrice items) -- Instead of this impure approach calculateTotal :: [Item] -> IO Price calculateTotal items = do forM items $ \item -> do logItemProcess item -- Side effect! return (itemPrice item) ... ``` - **Effect Localization**: When side effects are necessary, localize them to the boundaries of your application. Keep your core logic pure. ```haskell -- Good: Centralized effects at the edge main :: IO () main = do input <- readInput -- IO at the boundary let result = process input -- Pure core logic writeOutput result -- IO at the boundary ``` - **Resource Management**: Use higher-order functions like `bracket`, `withFile`, or `ResourceT` to ensure resources are properly acquired and released. ```haskell -- Ensures file is closed even if an exception occurs withConfigFile :: FilePath -> (Handle -> IO a) -> IO a withConfigFile path action = bracket (openFile path ReadMode) -- acquire hClose -- release action -- use ``` - **Error Handling**: Use types to represent errors rather than exceptions. Wrap impure code with `try`/`catch` and convert exceptions to domain-specific error types. ```haskell -- Domain-specific error type data AppError = FileNotFound FilePath | ParseError String | NetworkError -- Convert IO exceptions to domain errors readConfig :: FilePath -> IO (Either AppError Config) readConfig path = do result <- try (readFile path) case result of Left e -> return $ Left $ FileNotFound path Right content -> case parseConfig content of Nothing -> return $ Left $ ParseError "Invalid config" Just config -> return $ Right config ``` ### Type-Driven Development - **Newtype Wrappers**: Use `newtype` to create distinct types for values that might otherwise be confused. ```haskell -- Without newtypes processUser :: String -> Int -> String -> IO () -- What do these mean? -- With newtypes newtype UserId = UserId String newtype Age = Age Int newtype Email = Email String processUser :: UserId -> Age -> Email -> IO () -- Much clearer! ``` - **Smart Constructors**: Use smart constructors to enforce invariants and hide implementation details. ```haskell module Email (Email, mkEmail, emailToText) where newtype Email = Email { _unEmail :: Text } -- Private constructor -- Smart constructor with validation mkEmail :: Text -> Either String Email mkEmail txt | "@" `isInfixOf` txt = Right (Email txt) | otherwise = Left "Email must contain @" -- Accessor function emailToText :: Email -> Text emailToText (Email t) = t ``` - **Phantom Types**: Use phantom types to encode additional information in the type. ```haskell -- Phantom type for file access permissions data Permission = Read | Write | ReadWrite newtype File (p :: Permission) = File FilePath -- Operations that respect permissions readFile :: File p -> IO String readFile (File path) = -- ... writeFile :: File 'Write -> String -> IO () writeFile (File path) content = -- ... withWritableFile :: File 'Read -> (File 'Write -> IO a) -> IO a ``` ## Module Organization and API Design ### Clean Module Structure - **Hierarchical Module Structure**: Organize modules hierarchically (e.g., `App.Module.Submodule`) to make the codebase easier to navigate. ``` MyApp/ Core.hs -- Core functionality Core/ -- Implementation details Types.hs Operations.hs Database.hs -- Database facade Database/ -- Database implementations MySQL.hs PostgreSQL.hs ``` - **Facade Modules**: Create facade modules that re-export functionality from specialized modules. This allows implementation changes without affecting users of your API. ```haskell -- Database.hs (facade module) module Database ( Connection , QueryResult , connect , disconnect , query ) where import Database.Internal.Types import Database.Internal.Connection import Database.Internal.Query ``` - **Re-export Pattern**: Use selective re-exports to create a clean, focused API while hiding implementation details. ```haskell -- Clear separation between public API and internal details module MyLib ( -- * Core Types Widget(..) , WidgetId -- * Widget Creation , createWidget , defaultWidget -- * Widget Operations , updateWidget , renderWidget ) where import MyLib.Internal.Types import MyLib.Internal.Operations ``` ### API Design for Human Understanding - **Module Documentation**: Begin each module with a comprehensive Haddock comment that explains its purpose, main concepts, and usage examples. ```haskell {-| Module : Data.Parser Description : Parser combinators for structured data This module provides parser combinators for processing structured data. It supports: * Basic parsers for primitive types * Combinators for sequence and choice * Error reporting with context Example usage: @ parseJSON :: String -> Either ParseError JSONValue parseJSON input = runParser jsonValue input @ -} ``` - **Function Grouping**: Group related functions together and use Haddock section headers to organize the module documentation. ```haskell -- | Core data types -- | @Widget@ represents a UI element data Widget = ... -- | Operations on widgets -- | Create a new widget createWidget :: WidgetConfig -> Widget -- | Update widget properties updateWidget :: Widget -> WidgetUpdate -> Widget ``` - **Type Signatures as Documentation**: Write expressive type signatures that communicate intent. Use meaningful type and function names. ```haskell -- Less clear process :: [a] -> [(a, b)] -> [b] -> [c] -- More clear reconcileInventory :: [Product] -> [(Product, Quantity)] -> [Adjustment] -> [StockChange] ``` ## Advanced Type System Features for Safety and Clarity ### Type Classes and Constraints - **Type Class Constraints**: Use type class constraints to make requirements explicit and enable polymorphism. ```haskell -- Generic function that works with any monoid combineAll :: Monoid a => [a] -> a combineAll = foldr (<>) mempty -- Use with different monoid instances sumAll :: [Int] -> Int sumAll = getSum . combineAll . map Sum concatAll :: [[a]] -> [a] concatAll = combineAll -- Works because lists are monoids ``` - **Constraint Type Aliases**: Use ConstraintKinds to create aliases for common constraint combinations. ```haskell {-# LANGUAGE ConstraintKinds #-} -- Alias for common constraint combination type Serializable a = (ToJSON a, FromJSON a, Show a, Eq a) -- Simplified type signature storeEntity :: Serializable a => Connection -> a -> IO () storeEntity conn entity = do let json = toJSON entity -- Store the entity... ``` - **Multi-Parameter Type Classes**: Use MPTCs with functional dependencies or associated types to express relationships between types. ```haskell {-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies #-} -- Repository pattern with type safety class Repository r e | r -> e where save :: e -> r -> IO r findById :: Id e -> r -> IO (Maybe e) delete :: Id e -> r -> IO r -- Implementation for specific entity type instance Repository UserRepo User where save user repo = -- Implementation findById userId repo = -- Implementation delete userId repo = -- Implementation ``` ### Advanced Type Safety Features - **GADTs**: Use Generalized Algebraic Data Types to enforce invariants at the type level. ```haskell {-# LANGUAGE GADTs, DataKinds #-} -- Status for a request data Status = Pending | Approved | Rejected -- GADT that ensures status-specific operations data Request s where PendingRequest :: RequestId -> UserData -> Request 'Pending ApprovedRequest :: RequestId -> UserData -> ApproverInfo -> Request 'Approved RejectedRequest :: RequestId -> UserData -> RejectReason -> Request 'Rejected -- Type-safe operations approve :: ApproverInfo -> Request 'Pending -> Request 'Approved approve approver (PendingRequest id userData) = ApprovedRequest id userData approver -- Won't compile: -- approve :: ApproverInfo -> Request 'Rejected -> Request 'Approved ``` - **Phantom Types**: Use phantom types to add type-level tags without runtime overhead. ```haskell {-# LANGUAGE RankNTypes, KindSignatures #-} -- Phantom type for validation state data Validated data Unvalidated -- Email with validation state in the type newtype Email (s :: Type) = Email Text -- Smart constructor that returns validated email validateEmail :: Email Unvalidated -> Either String (Email Validated) validateEmail (Email txt) | "@" `isInfixOf` txt = Right (Email txt) | otherwise = Left "Invalid email address" -- Only validated emails can be sent sendEmail :: Email Validated -> Message -> IO () sendEmail (Email addr) msg = -- Implementation ``` - **Type Families**: Use type families to compute types based on other types. ```haskell {-# LANGUAGE TypeFamilies #-} -- Type family for result of an operation based on input type type family ResultOf a where ResultOf String = Int ResultOf Int = Double ResultOf (Maybe a) = Maybe (ResultOf a) -- Function with type that depends on input process :: a -> ResultOf a process = -- Implementation ``` ### Expressive Deriving Mechanisms - **DerivingVia**: Use DerivingVia for zero-boilerplate reuse of implementations. ```haskell {-# LANGUAGE DerivingVia, DerivingStrategies #-} -- Newtype wrapper for JSON serialization customization newtype UserName = UserName Text deriving stock (Show, Eq) deriving newtype (Semigroup, Monoid) deriving (ToJSON, FromJSON) via Text -- Composition of deriving strategies newtype UserId = UserId Int deriving stock (Show, Eq, Ord) deriving (ToJSON, FromJSON) via (Tagged "id" Int) ``` - **DerivingStrategies**: Be explicit about deriving mechanisms for clarity. ```haskell -- Explicitly specify deriving strategy data User = User { userId :: UserId , userName :: UserName , userEmail :: Email Validated } deriving stock (Show, Eq) deriving anyclass (ToJSON, FromJSON) deriving (Semigroup) via (GenericSemigroup User) ``` ## Composition Patterns for Readability ### Kleisli Composition for Monadic Pipelines Kleisli composition elegantly chains monadic operations, improving readability for complex workflows. ```haskell import Control.Arrow ((>>>), (<<<), Kleisli(..), runKleisli) -- Monadic functions (error handling, IO, etc.) validateInput :: Input -> Either Error ValidInput processData :: ValidInput -> Either Error ProcessedData generateReport :: ProcessedData -> Either Error Report -- Create Kleisli arrows for these functions validateK = Kleisli validateInput processK = Kleisli processData reportK = Kleisli generateReport -- Compose them into a clean pipeline pipeline :: Kleisli (Either Error) Input Report pipeline = validateK >>> processK >>> reportK -- Run the pipeline processBatch :: [Input] -> [Either Error Report] processBatch inputs = map (runKleisli pipeline) inputs -- Compare to nested approach: processManually :: Input -> Either Error Report processManually input = do validInput <- validateInput input processed <- processData validInput generateReport processed -- Less clear for complex pipelines ``` ### Function Composition for Pure Pipelines When working with pure functions, standard function composition offers clarity. ```haskell -- Pure data transformations normalize :: RawData -> NormalizedData analyze :: NormalizedData -> AnalysisResult format :: AnalysisResult -> FormattedOutput -- Direct composition pipeline :: RawData -> FormattedOutput pipeline = format . analyze . normalize -- Data flows from right to left, which can be counterintuitive -- Alternative: Forward composition with Data.Function import Data.Function ((&)) pipeline' :: RawData -> FormattedOutput pipeline' data = data & normalize -- First step & analyze -- Second step & format -- Final step ``` ## Error Handling Patterns ### Typed Errors with Monad Transformers Use explicit error types and monad transformers for comprehensive error handling. ```haskell -- Define a clear error hierarchy data AppError = FileSystemError FilePath IOError | ConfigError String | NetworkError ConnectionInfo String | ValidationError [String] | PermissionError UserId Resource deriving (Show, Eq) -- Application monad with built-in error handling type AppM a = ReaderT AppConfig (ExceptT AppError IO) a -- Helper for running the monad stack runAppM :: AppConfig -> AppM a -> IO (Either AppError a) runAppM config action = runExceptT (runReaderT action config) -- Convert IO exceptions to domain-specific errors safeFileOperation :: FilePath -> AppM ByteString safeFileOperation path = do result <- liftIO $ try $ readFile path case result of Left e -> throwError $ FileSystemError path e Right content -> return content ``` ### Railway-Oriented Programming with Either Use Either for explicit error handling in pure code without the complexity of monad transformers. ```haskell -- Define error types data ValidationError = MissingField String | InvalidFormat String String | OutOfRange String Int Int Int deriving (Show, Eq) -- Input validation function returning Either validateInput :: UserInput -> Either ValidationError ValidatedInput validateInput input = do name <- validateName (inputName input) age <- validateAge (inputAge input) email <- validateEmail (inputEmail input) pure ValidatedInput { validName = name , validAge = age , validEmail = email } -- Simple validation function validateAge :: Maybe Int -> Either ValidationError Int validateAge Nothing = Left (MissingField "age") validateAge (Just age) | age < 18 = Left (OutOfRange "age" age 18 120) | age > 120 = Left (OutOfRange "age" age 18 120) | otherwise = Right age ``` ### Smart Constructors for Validation Use smart constructors to ensure valid data at the type level. ```haskell -- Define a newtype with private constructor module Email (Email, mkEmail, emailToText) where newtype Email = Email { _unEmail :: Text } -- Private constructor -- Smart constructor returns Either for explicit error handling mkEmail :: Text -> Either EmailError Email mkEmail txt | T.null txt = Left EmailEmpty | not ("@" `T.isInfixOf` txt) = Left EmailMissingAt | not (hasDomainPart txt) = Left EmailInvalidDomain | otherwise = Right (Email $ T.toLower txt) -- Safe access functions emailToText :: Email -> Text emailToText (Email t) = t -- Because Email constructor is not exported, all Email values in your -- program are guaranteed to be valid ``` ### Nested Error Handling with MonadError Use MonadError for cleaner nested error handling. ```haskell import Control.Monad.Except -- Function signatures are cleaner with constraints instead of concrete types processTransaction :: (MonadError AppError m, MonadIO m) => Transaction -> m Receipt processTransaction tx = do -- validate will throw an error on invalid transaction validTx <- validate tx -- attempt to process, may throw network error result <- processPayment validTx `catchError` \e -> -- Add context to the error throwError $ PaymentError (transactionId tx) e -- generate receipt if successful generateReceipt tx result ``` ## Resource Management and Safety Patterns ### Bracket Pattern for Resource Safety Use the bracket pattern to ensure resources are properly acquired and released even when exceptions occur. ```haskell import Control.Exception (bracket) import System.IO -- Generic template for resource handling withResource :: IO a -- acquire resource -> (a -> IO ()) -- release resource -> (a -> IO b) -- use resource -> IO b withResource acquire release use = bracket acquire release use -- Example: File handling with automatic cleanup withFile' :: FilePath -> IOMode -> (Handle -> IO a) -> IO a withFile' path mode = bracket (openFile path mode) -- acquire hClose -- release -- Example: Database connection with transaction support withTransaction :: Connection -> (Connection -> IO a) -> IO a withTransaction conn action = bracket (do beginTransaction conn; return conn) -- start transaction (\c -> do rollback c; return ()) -- rollback on exception (\c -> do result <- action c -- run action commit c -- commit on success return result) ``` ### Resource Management with ResourceT For complex resource management scenarios, ResourceT from the resourcet package provides more flexibility. ```haskell import Control.Monad.Trans.Resource -- Create a computation that allocates and automatically frees resources complexOperation :: ResourceT IO Result complexOperation = do -- Register resources with cleanup actions (dbReleaseKey, dbConn) <- allocate (connectDB "database.db") -- acquire disconnectDB -- release (fileReleaseKey, fileHandle) <- allocate (openFile "output.txt" WriteMode) -- acquire hClose -- release -- Early release if needed release dbReleaseKey -- Resources automatically released when ResourceT exits liftIO $ processWithResources dbConn fileHandle -- Run the ResourceT computation runResourceOperation :: IO Result runResourceOperation = runResourceT complexOperation ``` ### Capability-Based Security Use the capability pattern to restrict access to sensitive operations. ```haskell -- Define capability tokens newtype FileReadCap = FileReadCap { allowedDirs :: [FilePath] } newtype FileWriteCap = FileWriteCap { writeDirs :: [FilePath] } -- Operations require explicit capabilities readFile' :: FileReadCap -> FilePath -> IO String readFile' cap path = do -- Verify path is in allowed directories allowed <- isPathAllowed (allowedDirs cap) path if allowed then readFile path else throwIO $ PermissionError $ "Cannot read: " ++ path -- Restricted capability creation rootCap :: IO FileReadCap rootCap = do -- Check if user has admin rights isAdmin <- checkAdminRights if isAdmin then return $ FileReadCap ["/"] -- Full access else return $ FileReadCap ["/home/user"] -- Limited access ``` ### Resource Pools for Performance Use resource pooling for expensive resources like database connections. ```haskell import Data.Pool -- Create a connection pool initConnectionPool :: Config -> IO (Pool Connection) initConnectionPool config = createPool (connect (dbHost config) (dbUser config)) -- create resource close -- destroy resource 1 -- stripes (for concurrency) 60 -- unused resource timeout (seconds) 10 -- maximum resources per stripe -- Use a resource from the pool withConnection :: Pool Connection -> (Connection -> IO a) -> IO a withConnection pool action = withResource pool action ``` ## Testing Patterns for Robust Code ### Property-Based Testing Use property-based testing to identify edge cases that unit tests might miss. ```haskell import Test.QuickCheck import Data.List (sort) -- Define properties that should hold for any input prop_reverseInvolutive :: [Int] -> Bool prop_reverseInvolutive xs = reverse (reverse xs) == xs prop_sortIdempotent :: [Int] -> Bool prop_sortIdempotent xs = sort (sort xs) == sort xs -- Test invariants that your functions should maintain prop_parseRenderRoundtrip :: Config -> Property prop_parseRenderRoundtrip config = parseConfig (renderConfig config) === Just config -- Run the tests main :: IO () main = do quickCheck prop_reverseInvolutive quickCheck prop_sortIdempotent quickCheck prop_parseRenderRoundtrip ``` ### Isolated Test Environments Create isolated, reproducible test environments for reliable testing. ```haskell import System.IO.Temp (withSystemTempDirectory) import System.FilePath (()) -- Helper for setting up a test environment withTestEnvironment :: (FilePath -> IO ()) -> IO () withTestEnvironment runTest = withSystemTempDirectory "test-dir" $ \tmpDir -> do -- Create test files and directories createDirectoryIfMissing True (tmpDir "src") createDirectoryIfMissing True (tmpDir "config") writeFile (tmpDir "src" "test.file") "test content" writeFile (tmpDir "config" "settings.json") "{\"mode\":\"test\"}" -- Run the test with the prepared environment runTest tmpDir -- Use it in hspec tests it "processes files correctly" $ withTestEnvironment $ \tmpDir -> do -- Configure app to use the temp directory let config = defaultConfig { rootDir = tmpDir } -- Run the application result <- runApp config -- Make assertions result `shouldBe` Success ``` ### Golden Tests for Output Verification Use golden testing to verify your outputs match expected templates. ```haskell import Test.Tasty.Golden (goldenVsString) import qualified Data.ByteString.Lazy as BL -- Test that generated output matches a "golden" file goldenOutputTest :: TestTree goldenOutputTest = goldenVsString "report generation" -- test name "test/golden/expected_report.json" -- golden file path (BL.fromStrict <$> generateReport testData) -- actual output -- For complex outputs like HTML, use a difference tool htmlGoldenTest :: TestTree htmlGoldenTest = goldenVsFileDiff "page rendering" -- test name diffCommand -- diff command to use "test/golden/expected.html" -- golden file "test/output/actual.html" -- actual output file (renderPage testData) -- action to generate actual output where diffCommand ref new = ["diff", "-u", ref, new] ``` ### Table-Driven Testing Use table-driven testing for testing multiple related cases concisely. ```haskell import Test.Hspec -- Define test cases as a list of input-output pairs testCases :: [(String, Int)] testCases = [ ("123", 123) , ("0", 0) , ("00123", 123) , ("+123", 123) , ("-123", -123) ] -- Test all cases using the same pattern spec :: Spec spec = describe "parseNumber" $ do forM_ testCases $ \(input, expected) -> it ("parses " ++ show input ++ " correctly") $ do parseNumber input `shouldBe` Right expected -- For more complex test cases, use records data ValidationTestCase = ValidationTestCase { testName :: String , testInput :: UserInput , expectedResult :: Either ValidationError ValidatedInput } validationTests :: [ValidationTestCase] validationTests = [ ValidationTestCase "valid input" (UserInput "John" (Just 30) "john@example.com") (Right $ ValidatedInput "John" 30 "john@example.com") , ValidationTestCase "missing age" (UserInput "John" Nothing "john@example.com") (Left $ MissingField "age") ] ``` ### Test Fixtures and Mocks Use fixtures and mocks to test code that depends on external systems. ```haskell -- Define a typecalss for database operations class Monad m => MonadDB m where queryUsers :: m [User] saveUser :: User -> m () -- Production implementation instance MonadDB IO where queryUsers = queryUsersFromDatabase saveUser = saveUserToDatabase -- Test implementation instance MonadDB (State TestDB) where queryUsers = gets testDBUsers saveUser user = modify $ \db -> db { testDBUsers = user : testDBUsers db } -- Example test it "creates user profile" $ do -- Set up initial DB state let initialDB = TestDB { testDBUsers = [] } -- Run operation with mock DB let (result, finalDB) = runState createUserProfile initialDB -- Assert the operation worked correctly length (testDBUsers finalDB) `shouldBe` 1 -- Function being tested uses constraint for testability createUserProfile :: MonadDB m => m User createUserProfile = do -- Implementation ``` ## Debugging and Maintainability Patterns ### Function Decomposition for Testability Break complex functions into smaller, testable parts that can be individually verified. ```haskell -- Original monolithic function (hard to test and debug) complexProcess :: Config -> [Input] -> IO [Output] complexProcess config inputs = do -- 100+ lines of complex logic with multiple responsibilities -- and many potential failure points... -- Refactored into testable components validateInputs :: [Input] -> Either ValidationError [ValidInput] validateInputs = traverse validateSingleInput processValidInputs :: Config -> [ValidInput] -> IO [ProcessedData] processValidInputs config = traverse (processOne config) generateOutputs :: [ProcessedData] -> [Output] generateOutputs = map convertToOutput -- Compose them back together with clear error handling complexProcess :: Config -> [Input] -> IO (Either Error [Output]) complexProcess config inputs = do case validateInputs inputs of Left validationError -> pure $ Left $ ValidationFailed validationError Right validInputs -> do processResult <- try $ processValidInputs config validInputs case processResult of Left ex -> pure $ Left $ ProcessingFailed ex Right processed -> pure $ Right $ generateOutputs processed ``` ### Layered Debugging Techniques Use a combination of tracing approaches for effective debugging. ```haskell import Debug.Trace (trace, traceShowId, traceM) import qualified System.IO as IO -- 1. Simple trace for basic logging (but doesn't clutter production code) withTracing :: Bool -> a -> String -> a withTracing True x msg = trace msg x withTracing False x _ = x -- 2. Effectful tracing for debugging monadic code processItems :: [Item] -> IO [Result] processItems = mapM $ \item -> do when debugMode $ traceM $ "Processing: " ++ show item result <- processItem item when debugMode $ traceM $ "Result: " ++ show result return result -- 3. Conditional file logging when trace output is too large logToFile :: String -> IO () logToFile msg = when debugMode $ IO.appendFile "debug.log" (msg ++ "\n") -- 4. TraceShowId for quick inspection of values in a pipeline calculateResults :: [Input] -> [Output] calculateResults = filter isValid >>> map preprocess >>> traceShowId -- See the values mid-pipeline >>> map calculate >>> filter isSignificant -- 5. Temporary function modification for deeper inspection -- Original function process :: Item -> Result process = step1 >>> step2 >>> step3 -- Modified during debugging process :: Item -> Result process item = let s1 = step1 item _ = trace ("After step1: " ++ show s1) () s2 = step2 s1 _ = trace ("After step2: " ++ show s2) () in step3 s2 ``` ### Typed Holes for Guided Development Use typed holes to let the compiler guide your implementation. ```haskell -- Start with the function type signature processTransaction :: UserId -> Transaction -> Either Error Receipt processTransaction userId transaction = _implementThis -- The compiler will tell you the expected type of _implementThis -- Gradually fill in implementation guided by holes processTransaction userId transaction = do user <- _getUser userId validated <- _validateTransaction user transaction _processPayment validated -- Each hole tells you what you need to implement next _getUser :: UserId -> Either Error User _getUser = ... _validateTransaction :: User -> Transaction -> Either Error ValidatedTransaction _validateTransaction = ... ``` ### Equational Reasoning and Step-by-Step Refactoring Use equational reasoning to verify code transformations. ```haskell -- Original code sum (map square xs) -- Step 1: Rewrite using function composition sum . map square $ xs -- Step 2: Introduce a specialized function sumOfSquares = sum . map square -- Verification: -- sum (map square xs) -- = sum . map square $ xs -- By function composition -- = sumOfSquares xs -- By definition -- More complex example: processList xs = filter p1 (map f (filter p2 xs)) -- Transform step by step: processList xs = (filter p1 . map f . filter p2) xs -- Extract function: processList = filter p1 . map f . filter p2 -- Each step preserves behavior but improves readability ``` ## Performance Patterns and Optimizations ### Hash Function Selection and Implementation Choose the appropriate hash function based on your actual requirements, not just defaults: ```haskell -- Non-cryptographic fast hashing (xxHash) when you just need speed import qualified Data.Digest.XXHash.FFI as XXH import Data.Hashable (hash) fastChecksum :: BS.ByteString -> Checksum fastChecksum content = let -- Use XXH3 hash function (extremely fast) hashVal = hash (XXH.XXH3 content) -- Handle potential negative hash values absHash = abs hashVal -- Convert to hex string representation hexStr = showHex absHash "" in Checksum hexStr -- Cryptographic hashing when security is required import qualified Crypto.Hash.SHA256 as SHA256 import qualified Data.ByteString.Base16 as Base16 secureChecksum :: BS.ByteString -> Checksum secureChecksum content = let hash = SHA256.hash content hexHash = Base16.encode hash in Checksum (show hexHash) ``` Performance considerations: - XXH3 can be 5-15x faster than cryptographic hashes like SHA-256 - For content identification, non-cryptographic hashes are usually sufficient - Be careful with hash values: they may be negative and need absolute value conversion - Abstract hash implementation behind a consistent interface ### Efficient ByteString Usage Use ByteString for efficient text and binary data handling. ```haskell import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as BS8 import qualified Data.ByteString.Lazy as BL import Data.Word (Word8) -- Converting between ByteString and String (avoid in performance-critical code) stringToBS :: String -> BS.ByteString stringToBS = BS8.pack -- For ASCII-only text -- For general Unicode text, use Text instead of String/ByteString import qualified Data.Text as T import qualified Data.Text.Encoding as TE textToBS :: T.Text -> BS.ByteString textToBS = TE.encodeUtf8 bsToText :: BS.ByteString -> Either String T.Text bsToText bs = case TE.decodeUtf8' bs of Left err -> Left $ "UTF-8 decoding error: " ++ show err Right text -> Right text -- Efficient file reading readLargeFile :: FilePath -> IO BL.ByteString readLargeFile = BL.readFile -- Lazy reading for large files -- Stream processing for large data processLargeFile :: FilePath -> FilePath -> IO () processLargeFile input output = do contents <- BL.readFile input BL.writeFile output $ BL.filter (/= 0) contents ``` ### Strict Fields for Memory Efficiency Use strictness annotations to avoid space leaks. ```haskell -- Without strictness, can cause space leaks data Configuration = Configuration { configPort :: Int , configHost :: String , configTimeout :: Int } -- With strictness annotations, more memory-efficient data Configuration' = Configuration' { configPort' :: !Int -- Strict field , configHost' :: !String , configTimeout' :: !Int } -- Strictness and UNPACK for numeric data data Point = Point { x :: {-# UNPACK #-} !Double -- Unpacked strict field , y :: {-# UNPACK #-} !Double } -- For record types with many fields {-# LANGUAGE StrictData #-} -- All fields strict by default data User = User { userId :: Int , userName :: String , userEmail :: String } ``` ### Fusion and Deforestation Take advantage of list fusion to eliminate intermediate data structures. ```haskell -- This will create an intermediate list naiveProcess :: [Int] -> Int naiveProcess xs = sum (filter even (map (*2) xs)) -- GHC can optimize this with list fusion fusedProcess :: [Int] -> Int fusedProcess = sum . filter even . map (*2) -- Even better: use foldr to fuse everything into a single pass singlePassProcess :: [Int] -> Int singlePassProcess = foldr (\x acc -> if even (x*2) then acc + (x*2) else acc) 0 -- For more control, use a specialized streaming library import qualified Streamly.Prelude as S streamProcess :: [Int] -> IO Int streamProcess xs = S.fold S.sum $ S.filter even $ S.map (*2) $ S.fromList xs ``` ### Handling Numeric Edge Cases Be aware of edge cases when working with numeric computations, especially hash functions: ```haskell -- Example: Converting hash values to hex strings import Data.Hashable (hash) import Numeric (showHex) -- INCORRECT: May fail on negative hash values toHexStringUnsafe :: Hashable a => a -> String toHexStringUnsafe x = showHex (hash x) "" -- Fails if hash x is negative -- CORRECT: Handle negative hash values toHexString :: Hashable a => a -> String toHexString x = let hashVal = hash x -- Take absolute value to ensure showHex works correctly absHash = abs hashVal in showHex absHash "" -- Alternative: Use Data.Bits for bit manipulation import Data.Bits ((.&.)) import Data.Word (Word64) -- This avoids negative numbers entirely by using Word64 toHexStringBits :: Hashable a => a -> String toHexStringBits x = let hashVal = hash x -- Convert to Word64 by masking with all bits set -- This preserves the exact bit pattern wordVal = fromIntegral hashVal .&. (maxBound :: Word64) in showHex wordVal "" ``` Common numeric pitfalls to handle: - Integer overflow/underflow - Division by zero - Negative values in functions expecting positives (like showHex) - Floating point precision errors - Range limitations in conversions between numeric types ### Lazy vs. Strict Evaluation Control Explicitly control evaluation strategy for better performance. ```haskell import Control.DeepSeq (NFData, force, ($!!)) -- Force full evaluation of a structure when needed processStrictly :: (NFData a) => [a] -> [a] processStrictly xs = force (map process xs) -- Manually force evaluation to specific depth data Tree a = Leaf a | Node (Tree a) (Tree a) forceTree :: Tree a -> () forceTree (Leaf _) = () forceTree (Node l r) = forceTree l `seq` forceTree r `seq` () -- Use bang patterns for strict evaluation in function arguments sumListStrict :: [Int] -> Int sumListStrict !xs = sum xs -- Force evaluation of xs -- Use BangPatterns language extension for more control {-# LANGUAGE BangPatterns #-} foldlStrict :: (b -> a -> b) -> b -> [a] -> b foldlStrict f !acc [] = acc foldlStrict f !acc (x:xs) = foldlStrict f (f acc x) xs ``` ## Build and Packaging Best Practices ### Cabal Configuration Properly configure your Cabal file for reliable builds and distribution. ```haskell -- Example cabal file structure with key sections name: my-project version: 0.1.0 synopsis: Short description of your project description: Longer, multi-line description of your project's purpose and features. license: MIT license-file: LICENSE author: Your Name maintainer: your.email@example.com category: Development build-type: Custom -- Use Custom for custom Setup.hs cabal-version: 2.0 -- Set up custom build if needed custom-setup setup-depends: base >= 4.7 && < 5, Cabal >= 2.0.0.2 && < 3.12, directory, filepath, process -- Documentation files that should be included in source distributions extra-source-files: README.md CHANGELOG.md examples/*.hs -- Files to be installed with the package data-files: templates/*.txt data/*.json -- Create an autogenerated module for accessing data-files auto-generated-modules: Paths_my_project -- Library component library -- Modules exposed to users of the library exposed-modules: MyProject MyProject.Core MyProject.Types -- Internal modules not exposed to users other-modules: MyProject.Internal.Util Paths_my_project -- Auto-generated module for data files -- Enable useful warnings ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -- Dependencies with version constraints build-depends: base >= 4.7 && < 5, aeson >= 1.4 && < 2.2, text >= 1.2 && < 2.1, containers >= 0.6 && < 0.7 ``` ### Version Range Best Practices Specify appropriate version ranges for dependencies to avoid compatibility issues. ``` -- For most dependencies, specify both lower and upper bounds build-depends: base >= 4.14 && < 5, text >= 1.2.4 && < 2.1, aeson >= 2.0 && < 2.2 -- Version range notation examples: -- == 1.0.0 -- Exactly version 1.0.0 -- >= 1.0 && < 1.1 -- Greater than or equal to 1.0 and less than 1.1 -- ^>= 1.0.0 -- Compatible with version 1.0.0 (>=1.0.0 && <1.1) -- ~> 1.0.0 -- Similar to ^>= but with more restriction ``` ### Documentation Integration Integrate documentation into your build process for better user experience. ```haskell -- In Setup.hs main = defaultMainWithHooks $ simpleUserHooks { postBuild = \args buildFlags pkg lbi -> do -- Run standard post-build first postBuild simpleUserHooks args buildFlags pkg lbi -- Generate documentation generateDocs pkg lbi } -- Generate embedded documentation from Markdown generateDocs :: PackageDescription -> LocalBuildInfo -> IO () generateDocs pkg lbi = do let docsDir = buildDir lbi "docs" createDirectoryIfMissing True docsDir -- Process each documentation file mapM_ (processDoc docsDir) ["README.md", "TUTORIAL.md", "API.md"] -- Use Haddock to generate API documentation let ghcProg = programPath (haddockProgram (withPrograms lbi)) pkgDb = packageDBFlags lbi runProcess ghcProg ["--haddock", ...] Nothing Nothing Nothing Nothing ``` ### Making Use of Flags and Conditionals Use flags and conditional compilation for flexible builds. ```haskell -- Define build flags in the cabal file flag strict description: Enable stricter GHC options default: False manual: True flag optimize description: Build with optimization default: True manual: False -- Use flags in the build configuration library if flag(strict) ghc-options: -Wall -Werror else ghc-options: -Wall if flag(optimize) ghc-options: -O2 else ghc-options: -O0 -- Conditional dependencies if os(windows) build-depends: Win32 else build-depends: unix -- For system-specific code if os(darwin) cpp-options: -DMACOS other-modules: System.MacOS.Specific elif os(linux) cpp-options: -DLINUX other-modules: System.Linux.Specific ``` ### Custom Setup Scripts ```haskell -- Setup.hs for custom build steps import Distribution.Simple import Distribution.Simple.Setup import Distribution.Simple.LocalBuildInfo import Distribution.PackageDescription main = defaultMainWithHooks simpleUserHooks { postBuild = \args flags pkg lbi -> do -- Run standard post-build first postBuild simpleUserHooks args flags pkg lbi -- Then run custom actions customAction args flags pkg lbi } -- Custom build/install actions customAction :: Args -> BuildFlags -> PackageDescription -> LocalBuildInfo -> IO () customAction _ _ pkg lbi = do -- Access package configuration let pkgName = unPackageName $ pkgName $ package pkg buildDir = buildDir lbi -- Execute custom build steps -- ... ``` ## Version Number Management ```haskell -- Access version from cabal file import qualified Paths_ as Meta import Data.Version (showVersion) -- Display version version :: String version = showVersion Meta.version ``` ## Best Practices - Favor pure Haskell implementations over shell commands - Document system dependencies explicitly - Load resource files from standardized locations, not hardcoded paths - Derive version information from the cabal file, not hardcoded - Use type applications for parametric types - Normalize paths for cross-platform compatibility - Add explicit type annotations for complex expressions - Integrate with standard system conventions (man pages, config directories) - Use Cabal's installation system rather than custom scripts for deployable artifacts ## Documentation Integration ```haskell -- In cabal file data-files: doc/*.md, -- Source files templates/*.txt -- Templates -- In Setup.hs import Distribution.PackageDescription import Distribution.Simple.LocalBuildInfo import Distribution.Simple.BuildPaths (autogenModulesDir) import Distribution.Simple.Utils (installOrdinaryFiles) -- Generate documentation during build postBuild _ _ pkg lbi = do let dataDirName = dataDir lbi docSrcDir = dataDirName "doc" docDestDir = buildDir lbi "doc" -- Generate docs from templates generateDocs docSrcDir docDestDir -- Install documentation to standard locations copyHook oldHook pkg_descr lbi hooks flags = do -- First do the standard copy oldHook pkg_descr lbi hooks flags -- Then copy documentation to proper locations let docDir = case os of "darwin" -> "/usr/local/share/doc/" ++ pkgName "linux" -> "/usr/share/doc/" ++ pkgName _ -> error "Unsupported OS" installOrdinaryFiles verbosity docDir [(buildDir lbi "doc", "*.html")] ``` ## Patterns for Human-AI Collaboration These patterns are particularly effective when working with AI assistants on Haskell projects. ### Leveraging Types for Verification Use type checking to verify collaboratively written code and catch misunderstandings early: ```haskell -- Example 1: Use phantom types to enforce usage patterns data Operation = Read | Write | ReadWrite -- File access with permission enforcement newtype File (p :: Operation) = File FilePath readFile :: File p -> IO String readFile (File path) = -- Implementation writeFile :: File 'Write -> String -> IO () writeFile (File path) content = -- Implementation -- Example 2: Use GADTs to restrict operations data DatabaseAction a where Query :: SQL -> DatabaseAction [Row] Update :: SQL -> DatabaseAction Int Transaction :: [DatabaseAction a] -> DatabaseAction [a] -- The return type enforces that queries return rows and updates return count runAction :: DatabaseAction a -> Connection -> IO a -- Example 3: Use newtypes to prevent confusion newtype FileReadCap = FileReadCap { allowedDirs :: [FilePath] } newtype FileWriteCap = FileWriteCap { writeDirs :: [FilePath] } -- Won't compile if permissions are mixed up readFile' :: FileReadCap -> FilePath -> IO String writeFile' :: FileWriteCap -> FilePath -> String -> IO () ``` By using these patterns, the AI can catch type errors during development rather than relying solely on runtime testing. The compiler becomes an active participant in the human-AI collaboration. ### Explicit Type Annotations Add type annotations to make intentions clear and guide AI inference, even when GHC can infer types. ```haskell -- Without annotation (ambiguous intention) processData input = map process . filter isValid $ input -- With annotation (clear intention) processData :: [InputData] -> [OutputData] processData input = map process . filter isValid $ input -- Intermediate type annotations for complex pipelines processData :: [InputData] -> [OutputData] processData input = let validData = filter isValid input :: [InputData] processedData = map process validData :: [IntermediateData] result = finalize <$> processedData :: [OutputData] in result ``` ### Named Function Parameters Use record syntax for complex parameter sets to make function usage self-documenting. ```haskell -- Hard to understand parameter meanings createUser :: String -> Int -> String -> Bool -> IO User createUser name age email verified = ... -- Parameters are self-documenting with records data CreateUserParams = CreateUserParams { userName :: String , userAge :: Int , userEmail :: String , isVerified :: Bool } createUser :: CreateUserParams -> IO User createUser params = ... -- Usage is clear and order-independent newUser <- createUser CreateUserParams { userName = "John" , userAge = 30 , userEmail = "john@example.com" , isVerified = True } ``` ### Consistent Error Handling Patterns Choose a consistent error handling approach and stick to it across the codebase. ```haskell -- Example with ExceptT pattern type AppM a = ExceptT AppError IO a -- Clear error hierarchy data AppError = ValidationError String | DatabaseError DBError | AuthError AuthenticationError | NotFoundError Resource deriving (Show, Eq) -- Helper functions for error handling whenM :: Monad m => m Bool -> m () -> m () whenM cond action = do result <- cond when result action -- Usage validateInput :: Input -> AppM ValidatedInput validateInput input = do whenM (pure $ null $ inputName input) $ throwError $ ValidationError "Name cannot be empty" whenM (pure $ inputAge input < 18) $ throwError $ ValidationError "Must be at least 18 years old" -- Create validated input after all checks pass return ValidatedInput { validName = inputName input , validAge = inputAge input } ``` ### Build System Patterns Use simpler build configurations when possible to ease maintenance. ```haskell -- Prefer Simple build-type over Custom when possible build-type: Simple -- Use common extensions across the project default-extensions: OverloadedStrings LambdaCase NamedFieldPuns RecordWildCards DeriveGeneric DeriveDataTypeable -- Only use custom Setup.hs when actually needed -- For example, to generate and install man pages ``` ### Library Selection for Maintainability Choose libraries that align with the project's complexity needs and maintainer expertise: ```haskell -- Prefer libraries with clear type signatures and good documentation -- GOOD: Easy for humans and AI to understand this API -- xxhash-ffi provides a clear API with good type signatures import qualified Data.Digest.XXHash.FFI as XXH import Data.Hashable (hash) calculateChecksum :: BS.ByteString -> String calculateChecksum content = show $ hash (XXH.XXH3 content) -- AVOID: Complex APIs with many type parameters or advanced features -- unless they're truly needed -- Overly complex for simple checksumming needs: import qualified Crypto.Hash as CH import qualified Crypto.Hash.Algorithms as CHA complexChecksum :: BS.ByteString -> String complexChecksum content = show (CH.hashWith CHA.SHA256 content :: CH.Digest CHA.SHA256) ``` Guidelines for library selection: - Choose libraries that match the project's complexity and maintainer expertise - Prefer libraries with clear documentation and simple, well-typed APIs - Consider the maintenance burden and dependency footprint - Avoid over-engineered solutions for simple problems - Document reasoning for library choices to help future maintainers ### Module Organization for Discovery Structure modules to facilitate code discovery by humans and AI assistants. ```haskell -- Organize hierarchically with explicit exports module MyApp ( -- * Core types AppConfig(..) , AppState(..) -- * Running the application , runApp , runAppWithConfig -- * Error handling , AppError(..) , handleError ) where -- Create index modules module MyApp.Database ( -- * Re-exports from all database modules module MyApp.Database.Connection , module MyApp.Database.Query , module MyApp.Database.Migration ) where import MyApp.Database.Connection import MyApp.Database.Query import MyApp.Database.Migration -- Function purpose is clear from name validateUserInput :: UserInput -> Either ValidationError ValidatedInput ``` ### Type-Level Documentation Embed information in types to make function behavior self-documenting. ```haskell -- Types convey information about the function's behavior authRequired :: HasAuth r => RIO r Resource adminOnly :: HasAdminAccess r => RIO r Resource -- Status-tracking in result type data VerificationStatus = Pending | Verified | Rejected data Email (s :: VerificationStatus) = Email Text sendEmail :: Email 'Verified -> Message -> IO () -- Directional data flow data Input data Processed data Output data Pipeline s a where Input :: a -> Pipeline 'Input a Process :: Pipeline 'Input a -> Pipeline 'Processed a Output :: Pipeline 'Processed a -> Pipeline 'Output a -- Function chains are guaranteed correct order by types pipeline :: Data -> Pipeline 'Output Result pipeline = Output . Process . Input ```