| name | lang-haskell-dev |
| description | 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. |
Haskell 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 | name :: Type -> Typename x = expression |
| Pattern match | case x of Pattern -> expr |
| List comprehension | [x * 2 | x <- [1..10], x > 5] |
| Lambda | \x -> x + 1 |
| Function composition | (f . g) x equals f (g x) |
| Type class constraint | func :: (Show a) => a -> String |
| Monadic bind | x >>= f or do { y <- x; ... } |
| Functor map | fmap f x or f <$> x |
| Applicative apply | f <*> x |
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 |
|---|---|---|
Int, Integer |
0 |
Literal |
Float, Double |
0.0 |
Literal |
String |
"" |
mempty |
Text |
"" |
T.empty |
[a] |
[] |
mempty |
Maybe a |
Nothing |
Constructor |
Map k v |
M.empty |
mempty |
Set a |
S.empty |
mempty |
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:
patterns-concurrency-dev- STM, async, parallel strategiespatterns-serialization-dev- Aeson, YAML, validation patternspatterns-metaprogramming-dev- Template Haskell, Generics