Function Composition Patterns
Point-Free Style
Point-free style (tacit programming) avoids naming parameters, instead emphasizing function composition.
-- With explicitly named parameters
sumSquares xs = sum (map (^2) xs)
-- Point-free style
sumSquares = sum . map (^2)When to use:
- For short, simple compositions
- When it improves readability
When to avoid:
- For complex transformations
- When the transformation isn’t obvious at a glance
Function Application with ($)
The $ operator avoids parentheses by having lowest precedence.
-- With parentheses
show (read (head (words "42 43 44"))))
-- With $ operator
show $ read $ head $ words "42 43 44"
-- Mixed style (often most readable)
show $ read $ head (words "42 43 44")Function Composition Chain
processData :: String -> [Int]
processData = map read . words . filter isDigitList Processing Patterns
Map-Filter Pattern
-- Process a list by filtering then mapping
evenSquares = map (^2) . filter even
-- With list comprehension
evenSquares' xs = [x^2 | x <- xs, even x]Accumulator Pattern
-- Using an accumulator for efficiency
sum' :: [Int] -> Int
sum' xs = go 0 xs
where
go acc [] = acc
go acc (x:xs) = go (acc + x) xsFold Patterns
-- Left fold for building up a result
sumWithFoldl = foldl (+) 0
-- Right fold for building a structure
mapWithFoldr f = foldr (\x acc -> f x : acc) []
-- Scans for intermediate results
runningSum = scanl (+) 0Dealing with Optional Values
-- Safely extracting from Maybe
maybeToList :: Maybe a -> [a]
maybeToList Nothing = []
maybeToList (Just x) = [x]
-- Default value for Maybe
fromMaybe :: a -> Maybe a -> a
fromMaybe def Nothing = def
fromMaybe _ (Just x) = x
-- Checking if all values are present
allPresent :: [Maybe a] -> Maybe [a]
allPresent = foldr combine (Just [])
where
combine Nothing _ = Nothing
combine (Just x) (Just xs) = Just (x:xs)Monadic Patterns
The Maybe Monad for Error Handling
lookupUser :: UserId -> Maybe User
getAddress :: User -> Maybe Address
formatAddress :: Address -> String
-- Chaining operations that might fail
getUserAddress :: UserId -> Maybe String
getUserAddress uid = do
user <- lookupUser uid
addr <- getAddress user
return (formatAddress addr)The List Monad for Non-determinism
-- Generate all possible pairs
pairs :: [a] -> [b] -> [(a, b)]
pairs xs ys = do
x <- xs
y <- ys
return (x, y)The State Monad for Stateful Computations
import Control.Monad.State
-- Generate unique IDs
type Counter = State Int
nextId :: Counter Int
nextId = do
id <- get
put (id + 1)
return id
-- Generate multiple IDs
generateIds :: Int -> Counter [Int]
generateIds n = replicateM n nextId
runCounter :: Counter a -> a
runCounter c = evalState c 0The Reader Monad for Configuration
import Control.Monad.Reader
type Config = String
type ConfigM = Reader Config
getConfig :: ConfigM Config
getConfig = ask
processWithConfig :: ConfigM String
processWithConfig = do
config <- getConfig
return ("Using config: " ++ config)
runConfig :: ConfigM a -> Config -> a
runConfig = runReaderThe Writer Monad for Logging
import Control.Monad.Writer
type Log = [String]
type LoggingM = Writer Log
logMessage :: String -> LoggingM ()
logMessage msg = tell [msg]
processWithLogs :: Int -> LoggingM Int
processWithLogs n = do
logMessage $ "Processing " ++ show n
let result = n * 2
logMessage $ "Result: " ++ show result
return result
runLogging :: LoggingM a -> (a, Log)
runLogging = runWriterData Structure Patterns
Smart Constructors
-- Data type with invariants
data EmailAddress = EmailAddress String deriving Show
-- Smart constructor enforces validation
mkEmail :: String -> Maybe EmailAddress
mkEmail s
| isValidEmail s = Just (EmailAddress s)
| otherwise = Nothing
where
isValidEmail = (&&) <$> elem '@' <*> (not . null)Newtypes for Type Safety
-- Raw type
type UserId = Int
-- Safer with newtype
newtype UserIdSafe = UserIdSafe Int deriving (Eq, Show)
-- Function that requires UserIdSafe
lookupUser :: UserIdSafe -> Maybe UserRecord Patterns
-- Creating records
data Person = Person { name :: String, age :: Int }
-- Updating records
setAge :: Int -> Person -> Person
setAge newAge person = person { age = newAge }
-- Pattern matching on records
isAdult :: Person -> Bool
isAdult Person{age} = age >= 18Algorithm Patterns
Divide and Conquer
-- Merge sort as an example of divide and conquer
mergeSort :: Ord a => [a] -> [a]
mergeSort [] = []
mergeSort [x] = [x]
mergeSort xs = merge (mergeSort left) (mergeSort right)
where
(left, right) = splitAt (length xs `div` 2) xs
merge [] ys = ys
merge xs [] = xs
merge (x:xs) (y:ys)
| x <= y = x : merge xs (y:ys)
| otherwise = y : merge (x:xs) ysDynamic Programming
-- Fibonacci with memoization
import Data.Function (fix)
import qualified Data.Map as M
fib :: Int -> Integer
fib = (map fibMemo [0..] !!)
where
fibMemo 0 = 0
fibMemo 1 = 1
fibMemo n = fib (n-1) + fib (n-2)
-- Another way using a map
fibDP :: Int -> Integer
fibDP = (memo M.!)
where
memo = M.fromList $ map (\n -> (n, f n)) [0..100]
f 0 = 0
f 1 = 1
f n = memo M.! (n-1) + memo M.! (n-2)Function Design Patterns
Continuation Passing Style
-- Direct style
factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n-1)
-- Continuation passing style
factorialCPS :: Integer -> (Integer -> r) -> r
factorialCPS 0 k = k 1
factorialCPS n k = factorialCPS (n-1) (\res -> k (n * res))
-- Using the CPS version
factorial' :: Integer -> Integer
factorial' n = factorialCPS n idPartial Application for Specialization
-- General purpose function
between :: Ord a => a -> a -> a -> Bool
between low high x = low <= x && x <= high
-- Specialized versions through partial application
isAdult :: Int -> Bool
isAdult = between 18 200
isValidTemperature :: Double -> Bool
isValidTemperature = between (-273.15) 1000Lifting Patterns
-- Lifting a function to work with Maybe
liftMaybe :: (a -> b) -> Maybe a -> Maybe b
liftMaybe _ Nothing = Nothing
liftMaybe f (Just x) = Just (f x)
-- Lifting a binary function to work with Maybe
liftMaybe2 :: (a -> b -> c) -> Maybe a -> Maybe b -> Maybe c
liftMaybe2 _ Nothing _ = Nothing
liftMaybe2 _ _ Nothing = Nothing
liftMaybe2 f (Just x) (Just y) = Just (f x y)
-- These are actually fmap and liftA2 for MaybeError Handling Patterns
Railway-Oriented Programming
type Result a = Either String a
validateAge :: Int -> Result Int
validateAge age
| age < 0 = Left "Age cannot be negative"
| age > 120 = Left "Age is unrealistic"
| otherwise = Right age
validateName :: String -> Result String
validateName name
| null name = Left "Name cannot be empty"
| length name < 2 = Left "Name too short"
| otherwise = Right name
data Person = Person { name :: String, age :: Int }
createPerson :: String -> Int -> Result Person
createPerson name age = do
validName <- validateName name
validAge <- validateAge age
return $ Person validName validAgeCollecting Multiple Errors
import Data.Semigroup
import Data.Validation
data ValidationError = AgeError String | NameError String deriving Show
validateAge :: Int -> Validation [ValidationError] Int
validateAge age
| age < 0 = Failure [AgeError "Age cannot be negative"]
| age > 120 = Failure [AgeError "Age is unrealistic"]
| otherwise = Success age
validateName :: String -> Validation [ValidationError] String
validateName name
| null name = Failure [NameError "Name cannot be empty"]
| length name < 2 = Failure [NameError "Name too short"]
| otherwise = Success name
createPerson :: String -> Int -> Validation [ValidationError] Person
createPerson name age =
Person <$> validateName name <*> validateAge ageTesting Patterns
Property-Based Testing
import Test.QuickCheck
-- Define properties
prop_reverseReverse :: [Int] -> Bool
prop_reverseReverse xs = reverse (reverse xs) == xs
prop_sortIdempotent :: [Int] -> Bool
prop_sortIdempotent xs = sort (sort xs) == sort xs
-- Testing with QuickCheck
main :: IO ()
main = do
quickCheck prop_reverseReverse
quickCheck prop_sortIdempotentCombinators for Building Tests
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck
main :: IO ()
main = defaultMain tests
tests :: TestTree
tests = testGroup "Tests"
[ unitTests
, propertyTests
]
unitTests :: TestTree
unitTests = testGroup "Unit tests"
[ testCase "Addition works" $
2 + 2 @?= 4
, testCase "List length" $
length [1,2,3] @?= 3
]
propertyTests :: TestTree
propertyTests = testGroup "Property tests"
[ testProperty "reverse . reverse == id" $
\xs -> reverse (reverse xs) == (xs :: [Int])
]Performance Patterns
Strict Evaluation for Better Performance
-- Lazy version (may cause stack overflow on large lists)
sum' :: [Int] -> Int
sum' [] = 0
sum' (x:xs) = x + sum' xs
-- Strict version with accumulator
sum'' :: [Int] -> Int
sum'' xs = go 0 xs
where
go !acc [] = acc
go !acc (x:xs) = go (acc + x) xsUnboxed Types for Numeric Operations
import Data.Vector.Unboxed as U
-- Using unboxed vectors for efficient numeric operations
dotProduct :: U.Vector Double -> U.Vector Double -> Double
dotProduct v1 v2 = U.sum $ U.zipWith (*) v1 v2Key Points to Remember
- Favor composition and point-free style for simple transformations
- Use appropriate monads to structure computations with effects
- Prefer pattern matching and algebraic data types for data manipulation
- Use smart constructors and newtypes for type safety
- Tail recursion helps avoid stack overflows
- Railway-oriented programming simplifies error handling flows
- Strict evaluation can improve performance for numeric computations