Introduction
Getting Started
- QuickStart
Patterns
- Languages
- Supported Languages
- Python
- Java
- JavaScript
- TypeScript
- Node.js
- React
- Fastify
- Next.js
- Terraform
- C#
- C++
- C
- Go
- Rust
- Swift
- React Native
- Spring Boot
- Kotlin
- Flutter
- Ruby
- PHP
- Scala
- Perl
- R
- Dart
- Elixir
- Erlang
- Haskell
- Lua
- Julia
- Clojure
- Groovy
- Fortran
- COBOL
- Pascal
- Assembly
- Bash
- PowerShell
- SQL
- PL/SQL
- T-SQL
- MATLAB
- Objective-C
- VBA
- ABAP
- Apex
- Apache Camel
- Crystal
- D
- Delphi
- Elm
- F#
- Hack
- Lisp
- OCaml
- Prolog
- Racket
- Scheme
- Solidity
- Verilog
- VHDL
- Zig
- MongoDB
- ClickHouse
- MySQL
- GraphQL
- Redis
- Cassandra
- Elasticsearch
- Security
- Performance
Integrations
- Code Repositories
- Team Messengers
- Ticketing
Enterprise
Haskell is a statically typed, purely functional programming language with type inference and lazy evaluation. It is designed for teaching, research, and industrial applications.
Haskell, despite being a powerful and elegant functional language, has several common anti-patterns that can lead to performance issues, maintainability problems, and bugs. Here are the most important anti-patterns to avoid when writing Haskell code.
-- Anti-pattern: String concatenation with ++
buildMessage :: [String] -> String
buildMessage [] = ""
buildMessage (x:xs) = x ++ " " ++ buildMessage xs -- O(n²) complexity
-- Better approach: Use a more efficient data structure
import Data.Text (Text)
import qualified Data.Text as T
buildMessage :: [Text] -> Text
buildMessage = T.intercalate (T.pack " ")
-- Or with String but using ShowS for efficiency
buildMessageS :: [String] -> String
buildMessageS xs = foldr (\x acc -> x ++ " " ++ acc) "" xs
Avoid using ++
for repeated string concatenation, as it has O(n²) complexity. Use more efficient data structures like Text
or ByteString
, or techniques like ShowS
or Builder
.
-- Anti-pattern: Partial functions
headOfList :: [a] -> a
headOfList xs = head xs -- Crashes on empty list
lookupValue :: Eq k => k -> [(k, v)] -> v
lookupValue k xs = fromJust (lookup k xs) -- Crashes if key not found
-- Better approach: Return Maybe or Either
headOfList :: [a] -> Maybe a
headOfList [] = Nothing
headOfList (x:_) = Just x
lookupValue :: Eq k => k -> [(k, v)] -> Maybe v
lookupValue k xs = lookup k xs
Avoid partial functions like head
, tail
, fromJust
, etc. that can crash on certain inputs. Instead, use pattern matching or return Maybe
or Either
to handle all possible cases.
-- Anti-pattern: Inefficient list processing
sumOfSquares :: [Int] -> Int
sumOfSquares xs = sum [x * x | x <- xs, x > 0] -- Creates intermediate list
-- Better approach: Use foldr/foldl'
sumOfSquares :: [Int] -> Int
sumOfSquares = foldl' (\acc x -> if x > 0 then acc + x * x else acc) 0
Avoid creating intermediate lists when processing data. Use higher-order functions like foldr
, foldl'
, or foldMap
for more efficient processing.
-- Anti-pattern: Not using strictness annotations
data Point = Point Double Double -- Lazy fields
sumPoints :: [Point] -> Point
sumPoints points = foldl addPoints (Point 0 0) points
where addPoints (Point x1 y1) (Point x2 y2) = Point (x1 + x2) (y1 + y2)
-- Better approach: Use strictness annotations
data Point = Point !Double !Double -- Strict fields
sumPoints :: [Point] -> Point
sumPoints = foldl' addPoints (Point 0 0)
where addPoints (Point x1 y1) (Point x2 y2) = Point (x1 + x2) (y1 + y2)
Use strictness annotations (!
) for data fields that should be evaluated eagerly, especially in numeric computations, to avoid space leaks and improve performance.
-- Anti-pattern: Using String for text processing
countLines :: String -> Int
countLines = length . lines
-- Better approach: Use Text or ByteString
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
countLines :: T.Text -> Int
countLines = length . T.lines
main :: IO ()
main = do
content <- TIO.readFile "file.txt"
print $ countLines content
Avoid using String
(which is a linked list of characters) for text processing. Use Text
or ByteString
for better performance with large text data.
-- Anti-pattern: Using type synonyms
type UserId = Int
type Username = String
lookupUser :: UserId -> IO (Maybe Username)
lookupUser uid = -- implementation
-- Prone to errors like this:
someFunction :: Username -> UserId -> IO ()
someFunction uid name = lookupUser name >>= -- Wrong order, but compiles!
-- Better approach: Use newtypes
newtype UserId = UserId Int deriving (Eq, Show)
newtype Username = Username String deriving (Eq, Show)
lookupUser :: UserId -> IO (Maybe Username)
lookupUser (UserId uid) = -- implementation
-- Now this won't compile:
someFunction :: Username -> UserId -> IO ()
someFunction name uid = lookupUser name -- Type error!
Use newtype
instead of type
for type aliases to get compile-time type checking and avoid mixing up values of the same underlying type but different semantic meanings.
-- Anti-pattern: Excessive point-free style
processData :: [Int] -> [Int]
processData = map (*2) . filter (>0) . takeWhile (<100) . dropWhile (==0)
-- Better approach: More readable style
processData :: [Int] -> [Int]
processData xs = map (*2) $ filter (>0) $ takeWhile (<100) $ dropWhile (==0) xs
-- Or even clearer with named steps
processData :: [Int] -> [Int]
processData xs = map (*2) validNumbers
where
withoutLeadingZeros = dropWhile (==0) xs
numbersUnder100 = takeWhile (<100) withoutLeadingZeros
validNumbers = filter (>0) numbersUnder100
Avoid excessive point-free style that makes code hard to read. Use named parameters and intermediate values when it improves clarity.
-- Anti-pattern: Not using record syntax
data Person = Person String Int String
getName :: Person -> String
getName (Person name _ _) = name
getAge :: Person -> Int
getAge (Person _ age _) = age
getEmail :: Person -> String
getEmail (Person _ _ email) = email
-- Better approach: Use record syntax
data Person = Person
{ name :: String
, age :: Int
, email :: String
}
-- Now we get accessor functions for free
-- And can create/update records more clearly
updateEmail :: String -> Person -> Person
updateEmail newEmail person = person { email = newEmail }
Use record syntax for data types with multiple fields to get accessor functions for free and make field access more explicit and maintainable.
-- Anti-pattern: Not using helpful language extensions
data Maybe a = Nothing | Just a
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just a) = Just (f a)
instance Applicative Maybe where
pure = Just
Nothing <*> _ = Nothing
(Just f) <*> x = fmap f x
instance Monad Maybe where
return = pure
Nothing >>= _ = Nothing
(Just a) >>= f = f a
-- Better approach: Use language extensions
{-# LANGUAGE DeriveFunctor, DeriveFoldable, DeriveTraversable #-}
data Maybe a = Nothing | Just a
deriving (Functor, Foldable, Traversable)
-- Or even better with more extensions
{-# LANGUAGE DerivingStrategies, GeneralizedNewtypeDeriving #-}
newtype UserId = UserId Int
deriving stock (Eq, Ord, Show)
deriving newtype (Num, Enum)
Use appropriate language extensions like DeriveFunctor
, RecordWildCards
, or OverloadedStrings
to make your code more concise and expressive.
-- Anti-pattern: Exposing constructors for invalid states
module Email (Email(..)) where
data Email = Email String
-- Client code can create invalid emails
invalidEmail = Email "not-an-email"
-- Better approach: Use smart constructors
module Email (Email, mkEmail, emailAddress) where
data Email = Email String
emailAddress :: Email -> String
emailAddress (Email addr) = addr
mkEmail :: String -> Maybe Email
mkEmail addr
| isValidEmail addr = Just (Email addr)
| otherwise = Nothing
isValidEmail :: String -> Bool
isValidEmail = -- validation logic
Use smart constructors to ensure that invalid states cannot be represented. Hide data constructors and expose only functions that create valid values.
-- Anti-pattern: Using IO unnecessarily
calculateTotal :: [Double] -> IO Double
calculateTotal prices = do
let subtotal = sum prices
let tax = subtotal * 0.1
let total = subtotal + tax
return total -- No IO needed here
-- Better approach: Pure function
calculateTotal :: [Double] -> Double
calculateTotal prices = subtotal + tax
where
subtotal = sum prices
tax = subtotal * 0.1
Keep functions pure (without IO) whenever possible. Only use the IO monad when you actually need to perform input/output operations.
-- Anti-pattern: Explicit error handling everywhere
processItem :: Item -> Either Error Result
processItems :: [Item] -> Either Error [Result]
processItems [] = Right []
processItems (x:xs) = case processItem x of
Left err -> Left err
Right result -> case processItems xs of
Left err -> Left err
Right results -> Right (result : results)
-- Better approach: Use appropriate monads
import Control.Monad.Except
processItem :: Item -> Either Error Result
processItems :: [Item] -> Either Error [Result]
processItems items = forM items processItem
-- Or with different monads for different concerns
import Control.Monad.Reader
import Control.Monad.Except
processItem :: ReaderT Config (ExceptT Error IO) Result
Use appropriate monads and monad transformers for different concerns like error handling (Either
, ExceptT
), configuration (Reader
), or state (State
).
-- Anti-pattern: Manual nested record updates
data Address = Address { street :: String, city :: String, zipCode :: String }
data Person = Person { name :: String, age :: Int, address :: Address }
updateZipCode :: String -> Person -> Person
updateZipCode newZip person =
person { address = (address person) { zipCode = newZip } }
-- Better approach: Use lenses
{-# LANGUAGE TemplateHaskell #-}
import Control.Lens
data Address = Address { _street :: String, _city :: String, _zipCode :: String }
data Person = Person { _name :: String, _age :: Int, _address :: Address }
makeLenses ''Address
makeLenses ''Person
updateZipCode :: String -> Person -> Person
updateZipCode newZip = address.zipCode .~ newZip
Use lenses (e.g., from the lens
package) for working with nested data structures, especially when you need to update deeply nested fields.
-- Anti-pattern: Duplicated functionality
serializeToJSON :: User -> String
serializeToJSON user = -- implementation
serializeToJSON :: Product -> String
serializeToJSON product = -- implementation
-- Better approach: Use type classes
class ToJSON a where
toJSON :: a -> String
instance ToJSON User where
toJSON user = -- implementation
instance ToJSON Product where
toJSON product = -- implementation
-- Now we can write generic functions
saveToFile :: ToJSON a => FilePath -> a -> IO ()
saveToFile path obj = writeFile path (toJSON obj)
Use type classes to define common interfaces for different types, enabling polymorphic functions and reducing code duplication.
-- Anti-pattern: Poor error messages
parseConfig :: String -> Config
parseConfig str = case decode str of
Nothing -> error "Parse failed" -- Unhelpful message
Just config -> config
-- Better approach: Descriptive errors
parseConfig :: String -> Either String Config
parseConfig str = case decode str of
Nothing -> Left $ "Failed to parse config: " ++ str
Just config -> Right config
-- Even better with custom error types
data ConfigError = InvalidFormat String | MissingField String | InvalidValue String String
deriving Show
parseConfig :: String -> Either ConfigError Config
Provide descriptive error messages and use structured error types. Avoid using error
or undefined
in production code.
-- Anti-pattern: Lazy I/O
processFile :: FilePath -> IO ()
processFile path = do
contents <- readFile path -- Lazy I/O
let lines = lines contents
-- Process lines
-- File might still be open here!
-- Better approach: Strict I/O or streaming
import qualified Data.ByteString as BS
processFile :: FilePath -> IO ()
processFile path = do
contents <- BS.readFile path -- Strict I/O
let lines = BS.split 10 contents -- Split on newline
-- Process lines
-- File is definitely closed here
-- Or using streaming libraries
import Streaming.Prelude as S
import qualified Streaming.ByteString as SB
processFile :: FilePath -> IO ()
processFile path = SB.readFile path
& SB.lines
& S.map processLine
& S.stdoutLn
Avoid lazy I/O which can lead to resource leaks and unpredictable performance. Use strict I/O or streaming libraries instead.
-- Anti-pattern: Everything in one file
-- Main.hs with thousands of lines
-- Better approach: Modular project structure
-- src/
-- MyApp/
-- Types.hs
-- Database.hs
-- API.hs
-- Utils.hs
-- Main.hs
Organize your code into modules with clear responsibilities. Follow a consistent project structure to make your codebase more maintainable.
-- Anti-pattern: Manual dependency management
-- Copying code from other projects
-- Using globally installed packages
-- Better approach: Use Cabal or Stack
-- package.yaml (for Stack)
name: my-project
version: 0.1.0.0
dependencies:
- base >= 4.7 && < 5
- text
- aeson
- containers
library:
source-dirs: src
executables:
my-project-exe:
main: Main.hs
source-dirs: app
dependencies:
- my-project
tests:
my-project-test:
main: Spec.hs
source-dirs: test
dependencies:
- my-project
- hspec
Use proper dependency management tools like Cabal or Stack to manage your project’s dependencies and build process.
-- Anti-pattern: Manual testing or no testing
main = do
let result = myFunction 42
print result -- Manual check
-- Better approach: Use testing frameworks
import Test.Hspec
import Test.QuickCheck
main :: IO ()
main = hspec $ do
describe "myFunction" $ do
it "returns correct result for specific input" $ do
myFunction 42 `shouldBe` expectedResult
it "satisfies key property" $ property $
\x -> myFunction x >= 0 -- Property-based test
Write proper tests using frameworks like HUnit, Hspec, or QuickCheck. Use property-based testing when appropriate.
-- Anti-pattern: Poor or no documentation
processData :: [a] -> [a]
processData xs = -- implementation
-- Better approach: Proper documentation
-- | Process a list of items according to business rules.
-- The function filters out invalid items and transforms the rest.
--
-- >>> processData [1, 2, 3, 4]
-- [2, 4, 6, 8]
--
-- >>> processData []
-- []
processData :: [a] -> [a]
processData xs = -- implementation
Document your code with Haddock comments. Include descriptions, examples, and edge cases to help users understand how to use your functions.