NWave nw-fp-haskell
Haskell language-specific patterns, GADTs, type classes, and effect systems
install
source · Clone the upstream repo
git clone https://github.com/nWave-ai/nWave
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/nWave-ai/nWave "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/nw/skills/nw-fp-haskell" ~/.claude/skills/nwave-ai-nwave-nw-fp-haskell-55db01 && rm -rf "$T"
manifest:
plugins/nw/skills/nw-fp-haskell/SKILL.mdsource content
FP in Haskell -- Functional Software Crafter Skill
Cross-references: fp-principles | fp-domain-modeling | pbt-haskell
When to Choose Haskell
- Best for: correctness-critical systems | compiler-enforced purity | maximum type safety | financial systems
- Not ideal for: teams needing fast onboarding | rapid prototyping | .NET/JVM platform requirements
[STARTER] Quick Setup
# Install GHCup (manages GHC, cabal, stack, HLS) curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh # Create project mkdir order-service && cd order-service && cabal init --interactive # Or: stack new order-service simple && stack build && stack test
Test runner:
cabal test or stack test. Add hspec, QuickCheck, hedgehog to build-depends.
[STARTER] Type System for Domain Modeling
Choice Types (Sum Types)
data PaymentMethod = CreditCard CardNumber ExpiryDate | BankTransfer AccountNumber | Cash deriving (Eq, Show)
Record Types and Newtypes
data Customer = Customer { customerId :: CustomerId , customerName :: CustomerName , customerEmail :: EmailAddress } deriving (Eq, Show) newtype OrderId = OrderId Int deriving (Eq, Ord, Show) newtype EmailAddress = EmailAddress Text deriving (Eq, Show)
newtype is erased at compile time -- zero runtime overhead, full type safety.
[STARTER] Validated Construction (Smart Constructors)
module Domain.Email (EmailAddress, mkEmailAddress, emailToText) where import Data.Text (Text) import qualified Data.Text as T newtype EmailAddress = EmailAddress Text deriving (Eq, Show) mkEmailAddress :: Text -> Either ValidationError EmailAddress mkEmailAddress raw | "@" `T.isInfixOf` raw = Right (EmailAddress raw) | otherwise = Left (InvalidEmail raw)
Export the type but not the constructor. Only
mkEmailAddress can create values.
[INTERMEDIATE] Composition Style
Function Composition (Right-to-Left)
-- (.) composes right-to-left processOrder :: RawOrder -> Either OrderError Confirmation processOrder = confirmOrder . priceOrder . validateOrder
Monadic Chaining with do-notation
placeOrder :: RawOrder -> Either OrderError Confirmation placeOrder raw = do validated <- validateOrder raw priced <- priceOrder validated confirmOrder priced
Applicative for Independent Validation
mkCustomer :: Text -> Text -> Either ValidationError Customer mkCustomer rawName rawEmail = Customer <$> mkCustomerId 0 <*> mkCustomerName rawName <*> mkEmailAddress rawEmail
Error-Accumulating Validation
import Data.Validation (Validation, failure, success) mkCustomerV :: Text -> Text -> Validation [ValidationError] Customer mkCustomerV rawName rawEmail = Customer <$> validateName rawName -- Validation [ValidationError] CustomerName <*> validateEmail rawEmail -- all errors collected, not short-circuited
Unlike
Either which stops at first error, Validation accumulates all failures via its Applicative instance.
[INTERMEDIATE] Effect Management
Haskell enforces purity at the compiler level.
IO in return type means side effects.
calculateTotal :: Order -> Money -- Pure: compiler guarantees no side effects calculateTotal order = sumOf (orderLines order) saveOrder :: Order -> IO () -- Impure: IO in the type saveOrder order = writeToDatabase order -- calculateTotal CANNOT call saveOrder -- compiler error
[ADVANCED] Three Layers Pattern (Hexagonal Architecture)
-- Layer 1: Pure domain (no IO, no effects) module Domain.Order (calculateDiscount, validateOrder) where calculateDiscount :: Order -> Discount calculateDiscount order | totalLines order > 10 = Discount 0.1 | otherwise = Discount 0.0 -- Layer 2: Effect interfaces (type classes as ports) class Monad m => OrderRepo m where findOrder :: OrderId -> m (Maybe Order) saveOrder :: Order -> m () -- Layer 3: IO implementations (adapters) instance OrderRepo IO where findOrder orderId = queryDatabase orderId saveOrder order = insertDatabase order
Effect libraries: Effectful (recommended starting point, best performance) | mtl (existing codebases) | Polysemy (algebraic effect semantics).
[INTERMEDIATE] Testing
Frameworks: QuickCheck (original PBT) | Hedgehog (integrated shrinking) | Hspec (BDD) | tasty (composable test tree). See pbt-haskell for detailed PBT patterns.
Property Test Example
import Test.Hspec import Test.QuickCheck spec :: Spec spec = describe "validateOrder" $ do it "round-trips through serialization" $ property $ \order -> deserializeOrder (serializeOrder order) === Right order it "validated orders always have positive totals" $ property $ \rawOrder -> case validateOrder rawOrder of Left _ -> discard Right valid -> orderTotal valid > Money 0
Custom Generator
import Data.Text (pack) import Test.QuickCheck genValidEmail :: Gen EmailAddress genValidEmail = do user <- listOf1 (elements ['a'..'z']) domain <- listOf1 (elements ['a'..'z']) pure (EmailAddress (pack (user ++ "@" ++ domain ++ ".com")))
[ADVANCED] Idiomatic Patterns
GADTs for State Machines
{-# LANGUAGE GADTs, DataKinds #-} data OrderState = Unvalidated | Validated | Priced data Order (s :: OrderState) where UnvalidatedOrder :: RawData -> Order 'Unvalidated ValidatedOrder :: ValidData -> Order 'Validated PricedOrder :: PricedData -> Order 'Priced -- Type-safe transitions: only validated orders can be priced priceOrder :: Order 'Validated -> Either PricingError (Order 'Priced) priceOrder (ValidatedOrder d) = Right (PricedOrder (addPricing d))
Lazy Evaluation for Decoupled Pipelines
eligibleOrders :: [Order] -> [Order] eligibleOrders = take 10 . filter isEligible . sortBy orderDate
Maturity and Adoption
- Steep learning curve: Monads, type classes, category-theory vocabulary create significant onboarding barrier. Budget extra ramp-up time.
- GHC extensions confusion: Over 100 language extensions; knowing which to enable requires experience. Start with
defaults.GHC2021 - Space leaks from laziness: Default lazy evaluation causes subtle memory issues. Requires profiling discipline and strict annotations.
- Smaller talent pool: Hiring Haskell developers harder than mainstream languages. Consider team sustainability before committing.
Common Pitfalls
- Lazy space leaks: Use
(strict) instead offoldl'
. Usefoldl
for strict accumulators.BangPatterns - String vs Text: Never use
([Char]) for real data. UseString
/Data.Text
.Data.ByteString - Orphan instances: Define instances in the module of the type or class. Use newtype wrappers otherwise.
- Over-abstracting with type-level programming: GADTs and type families increase compile times and error complexity. Use for genuine safety gains.