icit type signature:
processUser :: UserId -> IO (Either AppError User) β not inferred.
Type signatures serve as:
- Documentation (the primary kind)
- Compiler-checked contracts
- Guidance for type inference in complex expressions
Use newtype for type safety around primitives:
newtype UserId = UserId { unUserId :: Int } deriving (Show, Eq, Ord)
Not: type UserId = Int (type aliases provide no safety)
Enter fullscreen mode Exit fullscreen mode
Type inference is powerful but type signatures are still mandatory for top-level definitions. AI sometimes omits them to save space β they're not optional.
* * *
## [](#rule-5-pattern-matching-exhaustive-and-expressive)Rule 5: Pattern matching β exhaustive and expressive
Pattern matching rules:
- Always handle all constructors β enable
-Wincomplete-patterns in GHC options
- Use
LambdaCase for concise single-argument matches:
\case { Just x -> ...; Nothing -> ... }
- Deconstruct in function arguments, not in the body:
processUser (User uid name) = ... not processUser u = let uid = userId u in ...
- Use
@ patterns to bind the whole value while matching: processUser u@(User uid _) = ...
- Wildcards
_ only when the value is genuinely unused
Enter fullscreen mode Exit fullscreen mode
Exhaustive pattern matching with compiler warnings turns runtime errors into compile errors. AI often uses incomplete patterns or reaches for field accessors where destructuring is cleaner.
* * *
## [](#rule-6-functors-applicatives-monads-use-the-right-abstraction)Rule 6: Functors, Applicatives, Monads β use the right abstraction
Abstraction selection:
fmap / <$> when transforming a value inside a context (Functor)
<*> when applying a wrapped function to a wrapped value (Applicative)
>>= / do notation for sequential effects where each step depends on the previous (Monad)
traverse for t a -> (a -> f b) -> f (t b) (effects over a traversable)
sequence for t (f a) -> f (t a)
Prefer do notation for readability when there are 3+ monadic steps.
Prefer point-free style for simple compositions: f . g . h not \x -> f (g (h x)).
Enter fullscreen mode Exit fullscreen mode
The Functor/Applicative/Monad hierarchy exists to express precisely how much power you need. AI often reaches for `Monad`/`do` when `Applicative` or `fmap` is sufficient β the less powerful abstraction is usually clearer.
* * *
## [](#rule-7-text-not-string)Rule 7: Text, not String
String handling:
- Use
Text (from Data.Text) everywhere, not String ([Char])
- Enable
OverloadedStrings so string literals work as Text
- Use
Data.Text.pack/unpack only at system boundaries (IO, legacy APIs)
- For building text:
Data.Text.Builder or fmt library β not ++ concatenation
- For parsing:
attoparsec or megaparsec β not manual String manipulation
- ByteString for binary data β never treat
ByteString as text
Enter fullscreen mode Exit fullscreen mode
`String = [Char]` is a linked list of characters. It's the default for historical reasons and is terrible for performance. `Text` is the correct type for textual data. AI uses `String` because tutorials use `String`.
* * *
## [](#rule-8-records-and-lenses)Rule 8: Records and lenses
Record conventions:
- Use record syntax for data types with multiple fields
- Prefix field names with the type name to avoid ambiguity:
data User = User { userId :: UserId, userName :: Text, userEmail :: Email }
- Use
lens or optics for nested record access/update
- Avoid the
RecordWildCards extension β it makes imports invisible
- For large records: use
Generic + lens derivation
When using lenses:
user ^. userName not userName user
user & userName .~ "Alice" not user { userName = "Alice" }
Enter fullscreen mode Exit fullscreen mode
Record field names in Haskell pollute the module namespace without prefixing. Lenses provide composable access and update for nested data.
* * *
## [](#rule-9-io-and-effects-keep-pure-code-pure)Rule 9: IO and effects β keep pure code pure
Effect discipline:
- Maximize pure functions β if it doesn't need IO, don't put it in IO
- Use
ReaderT env IO as the application monad (not raw IO or a complex mtl stack)
- Keep the
IO boundary at the edges: parse input, process purely, render output
- Use
IORef only when mutation is genuinely required (rare)
- For config: pass explicit records, not global IORef or unsafePerformIO
For concurrent code: use async + stm (Software Transactional Memory).
Never use unsafePerformIO outside of FFI boundaries.
Enter fullscreen mode Exit fullscreen mode
"IO everywhere" is a beginner pattern. Pure functions are easier to test, reason about, and compose. The discipline of keeping IO at the edges is one of Haskell's greatest design patterns.
* * *
## [](#rule-10-testing-with-hspec-and-quickcheck)Rule 10: Testing with Hspec and QuickCheck
Testing:
- Hspec for unit and integration tests (BDD-style:
describe/it/shouldBe)
- QuickCheck for property-based testing β generate invariants, not just examples
hspec-discover for automatic spec discovery
- Test pure functions without IO β makes tests fast and deterministic
- For IO: use
hspec with before/after for setup/teardown
- Golden tests:
hspec-golden for output comparison
Property patterns to always write:
roundtrip: encode . decode = id
idempotence: f (f x) = f x
commutativity where applicable
Enter fullscreen mode Exit fullscreen mode
QuickCheck is one of Haskell's most valuable tools and is underused in AI-generated test suites. Property-based tests catch edge cases that example-based tests miss by definition.
* * *
## [](#rule-11-cabalstack-and-dependency-management)Rule 11: Cabal/Stack and dependency management
Build system: Cabal with cabal.project, or Stack with stack.yaml β pick one and be consistent.
cabal conventions:
- Pin GHC version in cabal.project:
with-compiler: ghc-9.6.x
- Use
cabal freeze for reproducible builds
- Separate library, executable, and test stanzas in .cabal file
- Enable warnings in all stanzas:
-Wall -Wcompat -Widentities
HLS (Haskell Language Server): configure in hie.yaml for IDE support.
Do NOT use cabal install globally β use cabal run or add to PATH via nix/ghcup.
Enter fullscreen mode Exit fullscreen mode
* * *
## [](#rule-12-deriving-and-generic)Rule 12: Deriving and Generic
Use deriving to avoid boilerplate:
deriving (Show, Eq, Ord, Generic)
For JSON: use aeson with Generic derivation:
instance ToJSON User; instance FromJSON User β no manual instances unless customizing.
For other typeclasses: prefer deriving via GeneralizedNewtypeDeriving or DerivingVia.
Do NOT write manual Show instances unless the output format is specifically required.
Do NOT write manual Eq instances unless the equality semantics differ from structural equality.
Enter fullscreen mode Exit fullscreen mode
Boilerplate `Show`/`Eq`/`Ord` instances are noise. `deriving` is the correct default. AI sometimes writes them manually.
* * *
## [](#rule-13-haddock-and-documentation)Rule 13: Haddock and documentation
Documentation:
- All exported functions must have Haddock comments
- Format:
-- | Description. Example: @functionName arg@
- Document the invariants and preconditions, not the implementation
- Use
@since tags for API additions
- Run
cabal haddock to verify documentation builds
Module exports: explicit export lists on all modules β no module Foo where without an export list.
Explicit exports make the API surface clear and prevent accidental exports.
Enter fullscreen mode Exit fullscreen mode
* * *
## [](#your-claudemd-starting-point)Your CLAUDE.md starting point
Haskell Project β AI Coding Rules
Safety
No partial functions: no head/tail/fromJust/read/error/undefined.
Use Maybe/Either/ExceptT for absence and failure. Total functions only.
Types
Explicit type signatures on all top-level definitions.
newtype over type aliases for domain types.
Text not String. OverloadedStrings enabled.
Style
Exhaustive pattern matching (-Wincomplete-patterns enforced).
LambdaCase for single-argument matches.
Pure functions maximized β IO only at edges.
ReaderT env IO as application monad.
Abstractions
Use fmap/Applicative when Monad is not needed.
traverse for effects over traversables.
Point-free for simple compositions.
Records
Prefix field names with type name. lens/optics for nested access.
Testing
Hspec + QuickCheck. Properties: roundtrip, idempotence. Pure functions tested without IO.
Build
GHC 9.6+. -Wall -Wcompat -Wincomplete-patterns. Explicit export lists on all modules.
Enter fullscreen mode Exit fullscreen mode
* * *
## [](#why-haskell-especially-needs-this)Why Haskell especially needs this
Haskell's type system can catch an enormous class of bugs at compile time β but only if you use it correctly. Partial functions, `String` instead of `Text`, and avoiding the abstraction hierarchy all leave power on the table.
`CLAUDE.md` is the difference between AI that uses Haskell and AI that uses Haskell well.
The full rules pack across 15+ languages is at [gumroad](https://oliviacraftlat.gumroad.com/l/skdgt) β $27.