Claude Code for Haskell: Pure Functional Programming and Type System Mastery — Claude Skills 360 Blog
Blog / Systems / Claude Code for Haskell: Pure Functional Programming and Type System Mastery
Systems

Claude Code for Haskell: Pure Functional Programming and Type System Mastery

Published: January 8, 2027
Read time: 10 min read
By: Claude Skills 360

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.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free