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 isDigit

List 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) xs

Fold 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 (+) 0

Dealing 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 0

The 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 = runReader

The 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 = runWriter

Data 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 User

Record 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 >= 18

Algorithm 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) ys

Dynamic 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 id

Partial 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) 1000

Lifting 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 Maybe

Error 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 validAge

Collecting 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 age

Testing 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_sortIdempotent

Combinators 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) xs

Unboxed 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 v2

Key Points to Remember

  1. Favor composition and point-free style for simple transformations
  2. Use appropriate monads to structure computations with effects
  3. Prefer pattern matching and algebraic data types for data manipulation
  4. Use smart constructors and newtypes for type safety
  5. Tail recursion helps avoid stack overflows
  6. Railway-oriented programming simplifies error handling flows
  7. Strict evaluation can improve performance for numeric computations