Haskell’s type system eliminates entire categories of runtime errors at compile time. Algebraic data types model domain precisely — invalid states become unrepresentable. Typeclasses define ad-hoc polymorphism without inheritance. Every function is pure by default; IO actions are explicit in the type. The do notation chains monadic computations while maintaining referential transparency. Servant defines REST APIs as Haskell types — the type checker verifies client-server compatibility. QuickCheck generates hundreds of test cases from property specifications. Claude Code generates Haskell data types, typeclass instances, monadic pipelines, Servant API handlers, and QuickCheck property tests for production Haskell services.
CLAUDE.md for Haskell Projects
## Haskell Stack
- GHC >= 9.8, Stack or Cabal for builds
- Extensions: OverloadedStrings, DeriveGeneric, GeneralizedNewtypeDeriving, ScopedTypeVariables
- Web: servant >= 0.20 with servant-server
- DB: persistent + postgresql-simple or hasql
- JSON: aeson with deriving via Generic
- Testing: hspec (BDD) + QuickCheck (properties) + tasty
- Concurrency: async + STM for composable concurrent state
Algebraic Data Types
-- src/Domain/Order.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
module Domain.Order where
import Data.Aeson (FromJSON, ToJSON)
import Data.Text (Text)
import Data.Time (UTCTime)
import GHC.Generics (Generic)
-- Sum type: an OrderStatus is EXACTLY ONE of these constructors
data OrderStatus
= Pending
| Processing
| Shipped ShippingInfo -- carries data
| Delivered UTCTime -- delivery timestamp
| Cancelled CancelReason
deriving (Show, Eq, Generic)
data ShippingInfo = ShippingInfo
{ trackingNumber :: Text
, carrier :: Text
, estimatedDays :: Int
} deriving (Show, Eq, Generic)
data CancelReason
= CustomerRequest
| OutOfStock
| PaymentFailed
| FraudDetected
deriving (Show, Eq, Generic)
-- Product type: an Order has ALL of these fields
data Order = Order
{ orderId :: OrderId
, customerId :: CustomerId
, items :: [OrderItem]
, total :: Money
, status :: OrderStatus
, createdAt :: UTCTime
} deriving (Show, Eq, Generic)
-- Newtypes prevent mixing up IDs
newtype OrderId = OrderId Text deriving (Show, Eq, Ord, Generic)
newtype CustomerId = CustomerId Text deriving (Show, Eq, Ord, Generic)
newtype Money = Money Int deriving (Show, Eq, Ord, Generic) -- cents
data OrderItem = OrderItem
{ productId :: Text
, productName :: Text
, quantity :: Int
, unitPrice :: Money
} deriving (Show, Eq, Generic)
instance FromJSON Order
instance ToJSON Order
instance FromJSON OrderStatus
instance ToJSON OrderStatus
-- etc.
-- Smart constructor: validates before constructing
mkOrder :: CustomerId -> [OrderItem] -> Either Text Order
mkOrder _ [] = Left "Order must have at least one item"
mkOrder cid items
| any (\i -> quantity i <= 0) items = Left "All quantities must be positive"
| otherwise = Right Order
{ orderId = OrderId "pending" -- assigned by DB
, customerId = cid
, items = items
, total = calculateTotal items
, status = Pending
, createdAt = undefined -- filled in IO
}
calculateTotal :: [OrderItem] -> Money
calculateTotal = Money . sum . map itemSubtotal
where
itemSubtotal item = let Money p = unitPrice item
in p * quantity item
Typeclasses
-- src/Domain/Typeclasses.hs — defining and using typeclasses
module Domain.Typeclasses where
import Data.Text (Text)
import qualified Data.Text as T
-- Typeclass: defines an interface
class Describable a where
describe :: a -> Text
class (Describable a) => Priceable a where
priceInCents :: a -> Int
priceFormatted :: a -> Text
priceFormatted x = "$" <> T.pack (show (fromIntegral (priceInCents x) / 100.0 :: Double))
-- Instances for our domain types
instance Describable OrderStatus where
describe Pending = "Order received, pending confirmation"
describe Processing = "Order is being processed"
describe (Shipped info) = "Shipped via " <> carrier info <> " (" <> trackingNumber info <> ")"
describe (Delivered at) = "Delivered"
describe (Cancelled r) = "Cancelled: " <> describeCancelReason r
describeCancelReason :: CancelReason -> Text
describeCancelReason CustomerRequest = "customer request"
describeCancelReason OutOfStock = "item out of stock"
describeCancelReason PaymentFailed = "payment failed"
describeCancelReason FraudDetected = "fraud detected"
-- Functor-like typeclass for domain transformations
class Transformable f where
transform :: (a -> b) -> f a -> f b
-- Typeclass constraint in function signatures
summarize :: (Describable a, Priceable a) => [a] -> Text
summarize items = T.intercalate "\n"
[ "Items: " <> T.pack (show (length items))
, "Total: " <> priceFormatted (head items) -- simplified
, describe (head items)
]
Monadic Pipelines
-- src/Service/OrderService.hs — composing with Monad
module Service.OrderService where
import Control.Monad.Except (ExceptT, throwError, runExceptT)
import Control.Monad.Reader (ReaderT, ask, runReaderT)
import Control.Monad.IO.Class (liftIO)
import Data.Text (Text)
-- Application monad stack: IO + Reader config + Either error
type AppM = ReaderT AppConfig (ExceptT AppError IO)
data AppConfig = AppConfig
{ dbPool :: ConnectionPool
, maxOrderAmt :: Int -- cents
}
data AppError
= NotFound Text
| ValidationError Text
| DatabaseError Text
| AmountExceedsLimit Int Int -- requested, limit
deriving (Show)
-- Monadic validation pipeline
createOrder :: CustomerId -> [OrderItem] -> AppM Order
createOrder customerId items = do
config <- ask -- Read from ReaderT environment
-- Validate amount limit
let total = sum $ map (\i -> let Money p = unitPrice i in p * quantity i) items
if total > maxOrderAmt config
then throwError $ AmountExceedsLimit total (maxOrderAmt config)
else pure ()
-- Check customer exists
customer <- liftIO $ findCustomer (dbPool config) customerId
case customer of
Nothing -> throwError $ NotFound "Customer not found"
Just c -> pure ()
-- Create in DB
now <- liftIO getCurrentTime
let order = Order
{ orderId = OrderId "tmp"
, customerId = customerId
, items = items
, total = Money total
, status = Pending
, createdAt = now
}
savedOrder <- liftIO $ insertOrder (dbPool config) order
case savedOrder of
Left err -> throwError $ DatabaseError err
Right saved -> pure saved
-- Run the monad stack
runApp :: AppConfig -> AppM a -> IO (Either AppError a)
runApp config action = runExceptT (runReaderT action config)
-- Point-free style: function composition without naming arguments
isHighValue :: Order -> Bool
isHighValue = (> Money 10000) . total
pendingOrders :: [Order] -> [Order]
pendingOrders = filter (isPending . status)
where isPending Pending = True
isPending _ = False
Servant Type-Safe API
-- src/API/OrderAPI.hs — type-safe REST API with Servant
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
module API.OrderAPI where
import Servant
import Data.Text (Text)
-- The API type IS the specification — mismatched client/server = compile error
type OrderAPI =
"orders" :> Get '[JSON] [Order]
:<|> "orders" :> ReqBody '[JSON] CreateOrderRequest :> Post '[JSON] Order
:<|> "orders" :> Capture "orderId" Text :> Get '[JSON] Order
:<|> "orders" :> Capture "orderId" Text :> "cancel" :> Post '[JSON] Order
data CreateOrderRequest = CreateOrderRequest
{ reqCustomerId :: Text
, reqItems :: [OrderItem]
} deriving (Show, Generic, FromJSON, ToJSON)
-- Handler implementation — must match API type exactly
orderServer :: AppConfig -> Server OrderAPI
orderServer config =
listOrdersH
:<|> createOrderH
:<|> getOrderH
:<|> cancelOrderH
where
run :: AppM a -> Handler a
run action = do
result <- liftIO $ runApp config action
case result of
Left (NotFound msg) -> throwError err404 { errBody = encode msg }
Left (ValidationError msg) -> throwError err422 { errBody = encode msg }
Left (DatabaseError msg) -> throwError err500 { errBody = encode msg }
Left (AmountExceedsLimit r l) ->
throwError err422 { errBody = encode $ "Amount " <> show r <> " exceeds limit " <> show l }
Right a -> pure a
listOrdersH = run listOrders
createOrderH req = run $ createOrder (CustomerId $ reqCustomerId req) (reqItems req)
getOrderH oid = run $ getOrder (OrderId oid)
cancelOrderH oid = run $ cancelOrder (OrderId oid)
app :: AppConfig -> Application
app config = serve (Proxy :: Proxy OrderAPI) (orderServer config)
QuickCheck Property Testing
-- test/OrderSpec.hs — property-based testing
module OrderSpec where
import Test.Hspec
import Test.QuickCheck
import Data.List (sort)
-- Arbitrary instances for generated test data
instance Arbitrary Money where
arbitrary = Money . abs <$> arbitrary
instance Arbitrary OrderItem where
arbitrary = OrderItem
<$> arbitrary -- productId
<*> arbitrary -- productName
<*> (getPositive <$> arbitrary) -- positive quantity
<*> arbitrary -- unitPrice
spec :: Spec
spec = do
describe "calculateTotal" $ do
it "returns zero for empty items" $
calculateTotal [] == Money 0
it "is commutative over item order" $
property $ \items ->
calculateTotal items == calculateTotal (reverse items)
it "total >= price of any single item" $
property $ \(NonEmpty items) ->
all (\i -> calculateTotal items >= unitPrice i) items
describe "mkOrder" $ do
it "rejects empty item list" $
mkOrder (CustomerId "c1") [] `shouldSatisfy` isLeft
it "accepts valid orders" $
property $ \(NonEmpty items) ->
all (\i -> quantity i > 0) items ==>
isRight (mkOrder (CustomerId "c1") items)
describe "Money ordering" $ do
it "satisfies transitivity" $
property $ \(a :: Money) b c ->
(a <= b && b <= c) ==> a <= c
isLeft :: Either a b -> Bool
isLeft (Left _) = True
isLeft _ = False
isRight :: Either a b -> Bool
isRight = not . isLeft
For the OCaml alternative that shares Haskell’s ML heritage and strong type system but with stricter effect discipline and wider systems use, consider the functional programming guide for ML-family languages. For the Crystal language that brings type safety and native performance with Ruby-inspired syntax (versus Haskell’s mathematical purity), see the Crystal guide. The Claude Skills 360 bundle includes Haskell skill sets covering ADTs, monadic pipelines, Servant APIs, and QuickCheck testing. Start with the free tier to try Haskell program generation.