Claude-skill-registry lang-haskell-dev
Foundational Haskell patterns covering pure functional programming, type system, type classes, monads, and common idioms. Use when writing Haskell code, understanding pure functions, working with Maybe/Either, leveraging the type system, or needing guidance on functional programming patterns. This is the entry point for Haskell development.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/lang-haskell-dev" ~/.claude/skills/majiayu000-claude-skill-registry-lang-haskell-dev && rm -rf "$T"
skills/data/lang-haskell-dev/SKILL.mdHaskell Fundamentals
Foundational Haskell patterns and core language features for pure functional programming. This skill serves as both a reference for common patterns and foundation for advanced Haskell development.
Overview
This skill covers:
- Pure functions and immutability
- Type system and type inference
- Type classes (Functor, Applicative, Monad)
- Pattern matching and guards
- List comprehensions and recursion
- Common monads (Maybe, Either, IO, State)
- Function composition and higher-order functions
- Lazy evaluation fundamentals
This skill does NOT cover:
- Advanced type system features (GADTs, Type Families, DataKinds)
- Lens and optics
- Concurrency and parallelism (STM, async)
- Template Haskell and metaprogramming
- Specific frameworks (Yesod, Servant, Scotty)
- Build tools (Cabal, Stack) - see language-specific tooling skills
Quick Reference
| Task | Pattern |
|---|---|
| Define function | <br> |
| Pattern match | |
| List comprehension | |
| Lambda | |
| Function composition | equals |
| Type class constraint | |
| Monadic bind | or |
| Functor map | or |
| Applicative apply | |
Core Concepts
Pure Functions
-- Pure function: same input always produces same output add :: Int -> Int -> Int add x y = x + y -- No side effects - this is NOT valid pure code: -- impureAdd x y = do -- print "Adding..." -- Side effect! -- return (x + y) -- Referential transparency: can replace call with result result1 = add 2 3 -- Always 5 result2 = add 2 3 -- Always 5 (same)
Immutability
-- Values cannot be changed x = 5 -- x = 6 -- Error: multiple declarations -- "Update" by creating new values data User = User { name :: String, age :: Int } updateAge :: Int -> User -> User updateAge newAge user = user { age = newAge } -- Original unchanged user1 = User "Alice" 25 user2 = updateAge 26 user1 -- user1 still has age 25
Type System
Type Inference
-- Explicit type signature (recommended) double :: Int -> Int double x = x * 2 -- Type inference (compiler deduces type) triple x = x * 3 -- Inferred: Num a => a -> a -- Polymorphic types identity :: a -> a identity x = x -- Works for any type: identity 5, identity "hello", etc.
Algebraic Data Types
-- Sum types (OR) data Shape = Circle Float | Rectangle Float Float | Triangle Float Float Float -- Product types (AND) data Point = Point Float Float -- Record syntax data Person = Person { firstName :: String , lastName :: String , age :: Int } deriving (Show, Eq) -- Using records person = Person "Alice" "Smith" 30 name = firstName person -- "Alice" older = person { age = 31 } -- Update syntax
Type Aliases
-- Simple alias type Name = String type Age = Int -- Parameterized alias type Pair a = (a, a) type AssocList k v = [(k, v)] -- Usage getName :: Person -> Name getName p = firstName p
Newtype
-- Zero-cost wrapper (compile-time only) newtype UserId = UserId Int deriving (Show, Eq) newtype Email = Email String deriving (Show, Eq) -- Type safety: can't mix UserId and Email processUser :: UserId -> String processUser (UserId id) = "User " ++ show id -- Can't accidentally pass wrong type userId = UserId 42 -- processUser 42 -- Error! processUser userId -- Ok
Pattern Matching
Basic Patterns
-- Match literals isZero :: Int -> Bool isZero 0 = True isZero _ = False -- Match constructors describeShape :: Shape -> String describeShape (Circle r) = "Circle with radius " ++ show r describeShape (Rectangle w h) = "Rectangle " ++ show w ++ "x" ++ show h describeShape (Triangle a b c) = "Triangle with sides " ++ show a ++ "," ++ show b ++ "," ++ show c -- Match lists listLength :: [a] -> Int listLength [] = 0 listLength (_:xs) = 1 + listLength xs -- First element pattern head' :: [a] -> Maybe a head' [] = Nothing head' (x:_) = Just x
Case Expressions
-- Case in expression describe :: Maybe Int -> String describe m = case m of Nothing -> "No value" Just x -> "Value: " ++ show x -- Nested patterns evalExpr :: Expr -> Int evalExpr expr = case expr of Lit n -> n Add e1 e2 -> evalExpr e1 + evalExpr e2 Mul e1 e2 -> evalExpr e1 * evalExpr e2
Guards
-- Boolean conditions classify :: Int -> String classify n | n < 0 = "negative" | n == 0 = "zero" | n < 10 = "small" | otherwise = "large" -- Pattern guards processUser :: Maybe User -> String processUser mu | Nothing <- mu = "No user" | Just u <- mu, age u >= 18 = "Adult: " ++ name u | Just u <- mu = "Minor: " ++ name u
Type Classes
Common Type Classes
-- Eq: Equality data Color = Red | Green | Blue deriving (Eq) result = Red == Green -- False -- Ord: Ordering data Priority = Low | Medium | High deriving (Eq, Ord) result = High > Low -- True -- Show: String representation data Point = Point Int Int deriving (Show) p = Point 3 4 str = show p -- "Point 3 4" -- Read: Parse from string value = read "42" :: Int
Functor
-- fmap: Apply function inside context -- fmap :: Functor f => (a -> b) -> f a -> f b -- Maybe Functor result1 = fmap (*2) (Just 5) -- Just 10 result2 = fmap (*2) Nothing -- Nothing -- List Functor result3 = fmap (*2) [1,2,3] -- [2,4,6] -- Operator form: <$> result4 = (*2) <$> Just 5 -- Just 10 result5 = (*2) <$> [1,2,3] -- [2,4,6] -- Function composition in Functor result6 = (+1) <$> (*2) <$> Just 5 -- Just 11
Applicative
-- <*>: Apply function in context to value in context -- pure: Lift value into context -- Maybe Applicative result1 = pure (+) <*> Just 3 <*> Just 4 -- Just 7 result2 = pure (+) <*> Nothing <*> Just 4 -- Nothing -- Applicative style result3 = (+) <$> Just 3 <*> Just 4 -- Just 7 -- Multiple arguments data User = User String Int createUser = User <$> Just "Alice" <*> Just 30 -- Just (User "Alice" 30) -- List Applicative (Cartesian product) result4 = (*) <$> [1,2] <*> [3,4] -- [3,4,6,8]
Monad
-- >>= (bind): Chain operations that return monadic values -- return: Lift value into monad (same as pure) -- Maybe Monad safeDivide :: Float -> Float -> Maybe Float safeDivide _ 0 = Nothing safeDivide x y = Just (x / y) calculation :: Maybe Float calculation = do a <- Just 10 b <- Just 2 result <- safeDivide a b return (result * 2) -- Just 10.0 -- Same with bind operator calculation' = Just 10 >>= \a -> Just 2 >>= \b -> safeDivide a b >>= \result -> return (result * 2) -- Either Monad (for error handling) type Error = String validateAge :: Int -> Either Error Int validateAge age | age < 0 = Left "Age cannot be negative" | age > 150 = Left "Age too high" | otherwise = Right age validateEmail :: String -> Either Error String validateEmail email | '@' `elem` email = Right email | otherwise = Left "Invalid email" createUser :: Int -> String -> Either Error User createUser age email = do validAge <- validateAge age validEmail <- validateEmail email return $ User validEmail validAge
Lists and Recursion
List Comprehensions
-- Basic comprehension squares = [x^2 | x <- [1..10]] -- With filter evenSquares = [x^2 | x <- [1..10], even x] -- Multiple generators pairs = [(x,y) | x <- [1..3], y <- [1..3]] -- [(1,1),(1,2),(1,3),(2,1),(2,2),(2,3),(3,1),(3,2),(3,3)] -- Dependent generators orderedPairs = [(x,y) | x <- [1..5], y <- [x..5]] -- [(1,1),(1,2),...,(5,5)] -- Multiple filters pythagoras = [(a,b,c) | a <- [1..20], b <- [a..20], c <- [b..20], a^2 + b^2 == c^2]
Recursive Functions
-- Sum of list sum' :: [Int] -> Int sum' [] = 0 sum' (x:xs) = x + sum' xs -- Filter filter' :: (a -> Bool) -> [a] -> [a] filter' _ [] = [] filter' p (x:xs) | p x = x : filter' p xs | otherwise = filter' p xs -- Map map' :: (a -> b) -> [a] -> [b] map' _ [] = [] map' f (x:xs) = f x : map' f xs -- Fold (right) foldr' :: (a -> b -> b) -> b -> [a] -> b foldr' _ acc [] = acc foldr' f acc (x:xs) = f x (foldr' f acc xs) -- Fibonacci fib :: Int -> Int fib 0 = 0 fib 1 = 1 fib n = fib (n-1) + fib (n-2)
Common List Functions
-- Construction and access list = 1 : 2 : 3 : [] -- [1,2,3] first = head [1,2,3] -- 1 rest = tail [1,2,3] -- [2,3] -- Transformation doubled = map (*2) [1,2,3] -- [2,4,6] evens = filter even [1,2,3,4] -- [2,4] sum = foldr (+) 0 [1,2,3,4] -- 10 reversed = reverse [1,2,3] -- [3,2,1] -- Combination combined = concat [[1,2], [3,4]] -- [1,2,3,4] flattened = concatMap (\x -> [x,x]) [1,2,3] -- [1,1,2,2,3,3] zipped = zip [1,2,3] ['a','b','c'] -- [(1,'a'),(2,'b'),(3,'c')] -- Selection taken = take 3 [1..10] -- [1,2,3] dropped = drop 3 [1..10] -- [4,5,6,7,8,9,10] split = splitAt 3 [1..10] -- ([1,2,3],[4,5,6,7,8,9,10])
Higher-Order Functions
Function Composition
-- Compose: (f . g) x = f (g x) addThenDouble = (*2) . (+1) result = addThenDouble 5 -- 12 -- Chain multiple functions process = filter even . map (*2) . filter (>0) result = process [-2,-1,0,1,2,3] -- [2,4,6] -- Point-free style -- Instead of: f x = g (h x) -- Write: f = g . h sumOfSquares :: [Int] -> Int sumOfSquares = sum . map (^2)
Partial Application
-- Functions are curried by default add :: Int -> Int -> Int add x y = x + y -- Partial application add5 :: Int -> Int add5 = add 5 result = add5 10 -- 15 -- Common pattern doubleAll = map (*2) filterPositive = filter (>0) result = doubleAll [1,2,3] -- [2,4,6] result = filterPositive [-1,0,1,2] -- [1,2]
Common Higher-Order Patterns
-- Apply function n times applyN :: Int -> (a -> a) -> a -> a applyN 0 _ x = x applyN n f x = applyN (n-1) f (f x) result = applyN 3 (*2) 5 -- 40 (5*2*2*2) -- Flip arguments flip' :: (a -> b -> c) -> (b -> a -> c) flip' f x y = f y x -- Use with sections subtractFrom10 = flip (-) 10 result = subtractFrom10 3 -- 7 (10 - 3)
Common Monads
Maybe
-- Represent optional values findUser :: Int -> Maybe User findUser 1 = Just (User "Alice" 30) findUser _ = Nothing -- Chain operations getUserEmail :: Int -> Maybe String getUserEmail userId = do user <- findUser userId return (email user) -- Handle Nothing getEmailOrDefault :: Int -> String getEmailOrDefault userId = case findUser userId of Just user -> email user Nothing -> "no-email@example.com" -- Maybe functions result1 = fromMaybe "default" (Just "value") -- "value" result2 = fromMaybe "default" Nothing -- "default" result3 = maybe "none" show (Just 42) -- "42"
Either
-- Represent computations that can fail parseAge :: String -> Either String Int parseAge str = case reads str of [(n, "")] -> if n >= 0 then Right n else Left "Age must be positive" _ -> Left "Not a valid number" -- Chain Either operations validateUser :: String -> String -> Either String User validateUser ageStr emailStr = do age <- parseAge ageStr email <- validateEmail emailStr return $ User email age -- Either functions result1 = either show (*2) (Right 5) -- 10 result2 = either show (*2) (Left "error") -- "error"
IO
-- IO actions are first-class values greeting :: IO () greeting = do putStrLn "What is your name?" name <- getLine putStrLn $ "Hello, " ++ name -- Read file readConfig :: FilePath -> IO String readConfig path = do contents <- readFile path return contents -- Write file writeLog :: String -> IO () writeLog message = do appendFile "log.txt" (message ++ "\n") -- Sequence IO actions main :: IO () main = do putStrLn "Starting..." result <- computation putStrLn $ "Result: " ++ show result putStrLn "Done"
State
import Control.Monad.State -- Stateful computation type Counter a = State Int a -- Increment counter increment :: Counter () increment = modify (+1) -- Get current count getCount :: Counter Int getCount = get -- Stateful computation computation :: Counter Int computation = do increment increment increment count <- getCount return count -- Run state result = runState computation 0 -- (3, 3) finalState = execState computation 0 -- 3 finalValue = evalState computation 0 -- 3
Lazy Evaluation
Infinite Lists
-- Infinite list of naturals naturals = [1..] -- Take first 10 first10 = take 10 naturals -- [1,2,3,4,5,6,7,8,9,10] -- Infinite Fibonacci fibs = 0 : 1 : zipWith (+) fibs (tail fibs) first10Fibs = take 10 fibs -- [0,1,1,2,3,5,8,13,21,34] -- Infinite repeat ones = repeat 1 cycle123 = cycle [1,2,3] -- [1,2,3,1,2,3,1,2,3,...]
Strictness
-- Lazy by default lazySum xs = if null xs then 0 else head xs + lazySum (tail xs) -- Force strict evaluation with $! strictSum xs = if null xs then 0 else head xs $! strictSum (tail xs) -- seq: Force evaluation forceEval x y = x `seq` y -- BangPatterns extension {-# LANGUAGE BangPatterns #-} strictFunc !x = x + 1 -- x evaluated immediately
Common Idioms
Pipeline Style
-- Use $ to avoid parentheses result = show $ sum $ map (*2) [1,2,3] -- Use & for left-to-right flow (Data.Function) import Data.Function ((&)) result = [1,2,3] & map (*2) & sum & show
Where vs Let
-- where: Definitions after expression circleArea r = pi * r^2 where pi = 3.14159 -- let: Definitions before expression circleArea' r = let pi = 3.14159 in pi * r^2 -- let in do-notation computation = do let x = 5 y = 10 return (x + y)
Operator Sections
-- Partially apply operators add5 = (+5) -- Add 5 to argument half = (/2) -- Divide argument by 2 double = (*2) -- Multiply argument by 2 -- Use with map doubled = map (*2) [1,2,3] -- [2,4,6]
Troubleshooting
Type Errors
Problem: Type mismatch
-- Error: Couldn't match expected type 'Int' with actual type '[Int]' badFunc :: Int -> Int badFunc x = [x] -- Returns list, not Int
Fix: Match return type:
goodFunc :: Int -> [Int] goodFunc x = [x]
Infinite Loops
Problem: Non-terminating recursion
-- Never terminates! badLength xs = 1 + badLength xs
Fix: Add base case:
goodLength [] = 0 goodLength (_:xs) = 1 + goodLength xs
Monad Type Errors
Problem: Couldn't match type 'Maybe a' with 'a'
-- Error: findUser returns Maybe User, not User badCode = name (findUser 1)
Fix: Extract with bind or pattern match:
goodCode = do user <- findUser 1 return (name user) -- Or: goodCode = case findUser 1 of Just user -> name user Nothing -> "Unknown"
Lazy Evaluation Issues
Problem: Stack overflow on large list
-- Lazy accumulation builds up thunks badSum = foldl (+) 0 [1..1000000]
Fix: Use strict fold:
import Data.List (foldl') goodSum = foldl' (+) 0 [1..1000000]
Module System
Module Basics
-- Module declaration (must match file path) -- File: src/MyApp/User.hs module MyApp.User ( User(..) -- Export type and all constructors , createUser -- Export function , validateEmail -- Export function ) where import Data.Text (Text) import qualified Data.Map as M import Data.List (sort, nub) -- Module contents... data User = User { name :: Text, email :: Text } createUser :: Text -> Text -> User createUser n e = User n e validateEmail :: Text -> Bool validateEmail = undefined -- implementation
Import Variations
-- Import everything import Data.List -- Import specific items import Data.List (sort, nub, groupBy) -- Import with hiding import Prelude hiding (head, tail) -- Qualified import (prevents name collisions) import qualified Data.Map as M import qualified Data.Text as T -- Use: M.lookup, T.pack, T.unpack -- Qualified with original name import qualified Data.ByteString.Lazy -- Use: Data.ByteString.Lazy.readFile -- Combined: import some, qualify others import Data.Text (Text) import qualified Data.Text as T
Export Control
-- Export everything (not recommended) module MyModule where -- Explicit exports (recommended) module MyModule ( -- Types User(..) -- Export type with all constructors , Config(Config) -- Export type with specific constructor , Connection -- Export type only (abstract) -- Functions , createUser , updateUser -- Re-exports , module Data.Text -- Re-export entire module ) where -- Internal/private by default internalHelper :: Int -> Int -- Not exported, private internalHelper x = x + 1
Hierarchical Modules
src/ ├── MyApp.hs -- module MyApp ├── MyApp/ │ ├── Types.hs -- module MyApp.Types │ ├── User.hs -- module MyApp.User │ └── Internal/ │ └── Utils.hs -- module MyApp.Internal.Utils
-- Re-export pattern for convenience -- File: src/MyApp.hs module MyApp ( module MyApp.Types , module MyApp.User ) where import MyApp.Types import MyApp.User -- Users can now: -- import MyApp (User, createUser, ...)
Package Structure
# package.yaml (hpack) or .cabal name: my-app version: 0.1.0.0 library: source-dirs: src exposed-modules: - MyApp - MyApp.Types - MyApp.User other-modules: - MyApp.Internal.Utils # Not exposed to consumers
Zero and Default Values
Default Type Class
import Data.Default -- Using Default type class data Config = Config { port :: Int , host :: String , debug :: Bool } instance Default Config where def = Config { port = 8080 , host = "localhost" , debug = False } -- Usage config1 = def :: Config -- All defaults config2 = def { port = 3000 } -- Override port config3 = def { debug = True, port = 80 } -- Override multiple
Monoid and Mempty
import Data.Monoid -- mempty: identity element for Monoid emptyList = mempty :: [a] -- [] emptyString = mempty :: String -- "" emptySum = mempty :: Sum Int -- Sum 0 emptyProduct = mempty :: Product Int -- Product 1 -- Custom monoid with default data Settings = Settings { timeout :: Maybe Int , retries :: Maybe Int } instance Semigroup Settings where a <> b = Settings { timeout = timeout b <|> timeout a , retries = retries b <|> retries a } instance Monoid Settings where mempty = Settings Nothing Nothing
Maybe for Optional Values
-- Maybe represents optional values data Maybe a = Nothing | Just a -- Common patterns findUser :: UserId -> Maybe User findUser uid = lookup uid users -- Default with fromMaybe import Data.Maybe (fromMaybe) getPort :: Config -> Int getPort cfg = fromMaybe 8080 (configPort cfg) -- Chain with <|> getEnv :: String -> Maybe String -> Maybe String getEnv key fallback = lookup key env <|> fallback <|> Just "default"
Empty/Zero Values by Type
| Type | Zero/Empty Value | Function |
|---|---|---|
, | | Literal |
, | | Literal |
| | |
| | |
| | |
| | Constructor |
| | |
| | |
Concurrency
Haskell provides powerful concurrency abstractions with strong safety guarantees. For cross-language comparison, see
patterns-concurrency-dev.
Lightweight Threads
import Control.Concurrent -- Spawn lightweight thread (green thread) main = do forkIO $ do threadDelay 1000000 -- 1 second in microseconds putStrLn "Hello from thread" putStrLn "Main thread continues" threadDelay 2000000 -- Wait for child -- Thread with MVar communication example = do result <- newEmptyMVar forkIO $ do value <- computeExpensive putMVar result value -- Block until result available answer <- takeMVar result print answer
Async for Structured Concurrency
import Control.Concurrent.Async -- Run two actions concurrently, wait for both main = do (result1, result2) <- concurrently (fetchUrl "http://example.com/1") (fetchUrl "http://example.com/2") print (result1, result2) -- Race: first to complete wins winner <- race (fetchFromServer1 key) (fetchFromServer2 key) -- Map concurrently results <- mapConcurrently fetchUrl urls -- With timeout maybeResult <- timeout 5000000 longComputation -- 5 second timeout
Software Transactional Memory (STM)
import Control.Concurrent.STM -- Transactional variables type Account = TVar Int -- Atomic transfer between accounts transfer :: Account -> Account -> Int -> STM () transfer from to amount = do fromBalance <- readTVar from when (fromBalance < amount) retry -- Blocks until condition met modifyTVar from (subtract amount) modifyTVar to (+ amount) -- Run transaction main = do account1 <- newTVarIO 1000 account2 <- newTVarIO 0 atomically $ transfer account1 account2 500 balances <- atomically $ do b1 <- readTVar account1 b2 <- readTVar account2 return (b1, b2) print balances -- (500, 500)
Parallel Evaluation
import Control.Parallel.Strategies -- Parallel map parMap :: (a -> b) -> [a] -> [b] parMap f xs = map f xs `using` parList rseq -- Parallel computation compute :: [Int] -> Int compute xs = sum squares `using` rpar where squares = map (^2) xs -- Spark parallel evaluation import Control.Parallel (par, pseq) parFib :: Int -> Int parFib n | n < 2 = n | otherwise = let x = parFib (n-1) y = parFib (n-2) in x `par` y `pseq` (x + y)
Metaprogramming
Haskell's metaprogramming uses Template Haskell for compile-time code generation. For cross-language comparison, see
patterns-metaprogramming-dev.
Template Haskell Basics
{-# LANGUAGE TemplateHaskell #-} import Language.Haskell.TH -- Generate function at compile time -- Creates: add5 x = x + 5 $(do let name = mkName "add5" let body = [| \x -> x + 5 |] [d| $(varP name) = $body |] ) -- Quote expressions expr :: Q Exp expr = [| 1 + 2 |] -- Represents the expression (1 + 2) -- Quote types myType :: Q Type myType = [t| Maybe Int |] -- Quote patterns myPat :: Q Pat myPat = [p| (x, y) |]
Deriving with Template Haskell
{-# LANGUAGE TemplateHaskell #-} import Data.Aeson.TH -- Generate JSON instances data User = User { userName :: String , userAge :: Int } $(deriveJSON defaultOptions ''User) -- Generates: instance FromJSON User where ... -- instance ToJSON User where ... -- With options $(deriveJSON defaultOptions{fieldLabelModifier = drop 4} ''User) -- Strips "user" prefix from JSON keys
Lens Generation
{-# LANGUAGE TemplateHaskell #-} import Control.Lens data Person = Person { _name :: String , _age :: Int } makeLenses ''Person -- Generates: name :: Lens' Person String -- age :: Lens' Person Int -- Usage updateAge :: Person -> Person updateAge = over age (+1) getName :: Person -> String getName = view name
GHC Generics (Alternative to TH)
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveAnyClass #-} import GHC.Generics import Data.Aeson -- Automatic deriving via Generics data Config = Config { configPort :: Int , configHost :: String } deriving (Generic, FromJSON, ToJSON) -- Works out of the box config = decode "{\"configPort\":8080,\"configHost\":\"localhost\"}"
Serialization
For cross-language serialization patterns and comparison, see
patterns-serialization-dev.
Aeson (JSON)
{-# LANGUAGE DeriveGeneric #-} import Data.Aeson import GHC.Generics -- Automatic JSON with Generics data User = User { name :: String , email :: String , age :: Int } deriving (Generic, Show) instance FromJSON User instance ToJSON User -- Encode/decode encodeUser :: User -> ByteString encodeUser = encode decodeUser :: ByteString -> Maybe User decodeUser = decode
Custom JSON Instances
import Data.Aeson import Data.Aeson.Types data Status = Active | Inactive | Pending instance ToJSON Status where toJSON Active = String "active" toJSON Inactive = String "inactive" toJSON Pending = String "pending" instance FromJSON Status where parseJSON = withText "Status" $ \t -> case t of "active" -> return Active "inactive" -> return Inactive "pending" -> return Pending _ -> fail "Invalid status" -- Complex type with field renaming data Config = Config { configPort :: Int , configHost :: String } instance ToJSON Config where toJSON (Config p h) = object [ "port" .= p , "host" .= h ] instance FromJSON Config where parseJSON = withObject "Config" $ \v -> Config <$> v .: "port" <*> v .: "host"
Aeson Options
{-# LANGUAGE TemplateHaskell #-} import Data.Aeson.TH data ApiResponse = ApiResponse { responseStatus :: String , responseData :: Value , responseTimestamp :: Int } $(deriveJSON defaultOptions { fieldLabelModifier = camelTo2 '_' . drop 8 -- Remove "response" prefix, snake_case , omitNothingFields = True } ''ApiResponse) -- Produces: {"status": "...", "data": ..., "timestamp": ...}
YAML
import Data.Yaml -- Same types work with YAML (Aeson-based) config <- decodeFileThrow "config.yaml" :: IO Config -- Encode to YAML encodeFile "output.yaml" config
Validation
-- Manual validation with Either validateUser :: User -> Either String User validateUser u | null (name u) = Left "Name cannot be empty" | age u < 0 = Left "Age must be non-negative" | '@' `notElem` email u = Left "Invalid email" | otherwise = Right u -- With Validation Applicative import Data.Validation validateUser' :: User -> Validation [String] User validateUser' u = User <$> validateName (name u) <*> validateAge (age u) <*> validateEmail (email u) where validateName n | null n = Failure ["Name cannot be empty"] | otherwise = Success n -- ... etc
Build and Dependencies
Cabal
-- my-app.cabal cabal-version: 2.4 name: my-app version: 0.1.0.0 license: MIT author: Your Name common shared ghc-options: -Wall default-language: Haskell2010 library import: shared exposed-modules: MyApp MyApp.Types other-modules: MyApp.Internal build-depends: base >= 4.14 && < 5 , text >= 1.2 , aeson >= 2.0 , containers hs-source-dirs: src executable my-app import: shared main-is: Main.hs build-depends: base , my-app -- Depend on library hs-source-dirs: app test-suite my-app-test import: shared type: exitcode-stdio-1.0 main-is: Spec.hs build-depends: base , my-app , hspec >= 2.7 , QuickCheck hs-source-dirs: test
Stack
# stack.yaml resolver: lts-21.0 # Stackage snapshot packages: - . extra-deps: - some-package-1.0.0 - git: https://github.com/user/repo commit: abc123 # package.yaml (hpack format, generates .cabal) name: my-app version: 0.1.0.0 dependencies: - base >= 4.14 && < 5 - text - aeson library: source-dirs: src executables: my-app: main: Main.hs source-dirs: app dependencies: - my-app tests: my-app-test: main: Spec.hs source-dirs: test dependencies: - my-app - hspec - QuickCheck
Common Commands
# Cabal cabal init # Initialize new project cabal build # Build project cabal run # Build and run executable cabal test # Run tests cabal repl # Start REPL with project loaded cabal install --lib aeson # Install library globally # Stack stack new my-project # Create new project stack build # Build project stack run # Build and run stack test # Run tests stack ghci # REPL with project stack install # Install executables
GHC Options
-- Common GHC options ghc-options: -Wall -- Enable all warnings -Wcompat -- Warn about future incompatibilities -Wincomplete-patterns -- Warn about incomplete patterns -Wincomplete-uni-patterns -Wredundant-constraints -O2 -- Optimization level 2 -threaded -- Enable threaded runtime -rtsopts -- Enable RTS options -with-rtsopts=-N -- Use all CPU cores
Testing
HSpec
-- test/Spec.hs {-# OPTIONS_GHC -F -pgmF hspec-discover #-} -- test/MyApp/UserSpec.hs module MyApp.UserSpec where import Test.Hspec import MyApp.User spec :: Spec spec = do describe "createUser" $ do it "creates a user with the given name" $ do let user = createUser "Alice" "alice@example.com" userName user `shouldBe` "Alice" it "creates a user with the given email" $ do let user = createUser "Alice" "alice@example.com" userEmail user `shouldBe` "alice@example.com" describe "validateEmail" $ do it "returns True for valid email" $ do validateEmail "user@example.com" `shouldBe` True it "returns False for email without @" $ do validateEmail "invalid" `shouldBe` False
HSpec Matchers
import Test.Hspec spec :: Spec spec = do describe "Matchers" $ do -- Equality it "shouldBe" $ 1 + 1 `shouldBe` 2 -- Boolean it "shouldSatisfy" $ 5 `shouldSatisfy` (> 0) -- Lists it "shouldContain" $ [1,2,3] `shouldContain` [2] it "shouldMatchList" $ [1,2,3] `shouldMatchList` [3,1,2] -- Maybe it "shouldBe Just" $ Just 5 `shouldBe` Just 5 it "shouldBe Nothing" $ (Nothing :: Maybe Int) `shouldBe` Nothing -- Exceptions it "shouldThrow" $ evaluate (error "boom") `shouldThrow` anyException -- Approximate equality it "shouldSatisfy approx" $ 3.14159 `shouldSatisfy` (\x -> abs (x - pi) < 0.001)
QuickCheck (Property-Based Testing)
import Test.QuickCheck import Test.Hspec import Test.Hspec.QuickCheck spec :: Spec spec = do describe "reverse" $ do prop "reversing twice gives original" $ \xs -> reverse (reverse xs) == (xs :: [Int]) prop "length is preserved" $ \xs -> length (reverse xs) == length (xs :: [Int]) describe "sort" $ do prop "result is sorted" $ \xs -> isSorted (sort (xs :: [Int])) prop "length is preserved" $ \xs -> length (sort xs) == length (xs :: [Int]) prop "all elements preserved" $ \xs -> sort (xs :: [Int]) `shouldMatchList` xs isSorted :: Ord a => [a] -> Bool isSorted [] = True isSorted [_] = True isSorted (x:y:xs) = x <= y && isSorted (y:xs)
Custom Generators
import Test.QuickCheck -- Generator for positive integers positiveInt :: Gen Int positiveInt = abs <$> arbitrary `suchThat` (> 0) -- Generator for valid emails validEmail :: Gen String validEmail = do user <- listOf1 $ elements ['a'..'z'] domain <- listOf1 $ elements ['a'..'z'] return $ user ++ "@" ++ domain ++ ".com" -- Use with forAll prop_positiveSquare :: Property prop_positiveSquare = forAll positiveInt $ \n -> n * n > 0 -- Arbitrary instance for custom type data User = User String Int instance Arbitrary User where arbitrary = User <$> listOf1 (elements ['a'..'z']) <*> choose (0, 120)
Hedgehog (Alternative Property Testing)
import Hedgehog import qualified Hedgehog.Gen as Gen import qualified Hedgehog.Range as Range prop_reverse :: Property prop_reverse = property $ do xs <- forAll $ Gen.list (Range.linear 0 100) Gen.alpha reverse (reverse xs) === xs prop_sort :: Property prop_sort = property $ do xs <- forAll $ Gen.list (Range.linear 0 100) (Gen.int $ Range.linear 0 1000) let sorted = sort xs assert $ isSorted sorted length sorted === length xs
Cross-Cutting Patterns
For cross-language comparison and translation patterns, see:
- STM, async, parallel strategiespatterns-concurrency-dev
- Aeson, YAML, validation patternspatterns-serialization-dev
- Template Haskell, Genericspatterns-metaprogramming-dev