FP in Haskell -- Functional Software Crafter Skill
SkillDev toolsHaskell language-specific patterns, GADTs, type classes, and effect systems
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the FP in Haskell -- Functional Software Crafter Skill skill
What this skill tells your AI
The instructions your AI receives, as published by nwave-ai/nwave in nWave/skills/nw-fp-haskell/SKILL.md and read by ahel’s review.
Cross-references: fp-principles | fp-domain-modeling | property-based-testing
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 property-based-testing 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
GHC2021defaults. - 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
foldl'(strict) instead offoldl. UseBangPatternsfor strict accumulators. - String vs Text: Never use
String([Char]) for real data. UseData.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.
Signals
- GitHub stars
- 610
- Forks
- 64
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
nw-fp-haskell- Source
- github.com/nwave-ai/nwave