Haskell Pure Func
In this tutorial, you'll learn about Haskell Pure Func Quick Fix. We cover key concepts, practical examples, and best practices.
Haskell pure functions are the bedrock of referential transparency: calling a pure function with the same arguments always returns the same result. Breaking this contract with unsafePerformIO, global mutable state, or FFI calls causes unpredictable behavior and makes testing impossible.
The Wrong Way
-- Wrong: using unsafePerformIO makes this impure
import System.IO.Unsafe (unsafePerformIO)
import Data.IORef (newIORef, readIORef, modifyIORef')
globalCounter :: IORef Int
globalCounter = unsafePerformIO (newIORef 0)
{-# NOINLINE globalCounter #-}
impureAdd :: Int -> Int
impureAdd x = unsafePerformIO $ do
modifyIORef' globalCounter (+1)
c <- readIORef globalCounter
return (x + c)
Output:
-- First call: impureAdd 5 returns 6
-- Second call: impureAdd 5 returns 7
-- NOT PURE! Same input, different output
The Right Way
-- Right: keep pure functions pure
pureAdd :: Int -> Int -> Int
pureAdd x y = x + y
-- Move state to explicit parameter
addWithCounter :: Int -> Int -> (Int, Int)
addWithCounter x count = (x + count + 1, count + 1)
Output:
pureAdd 5 0 → 5
pureAdd 5 0 → 5 -- Same input, same output every time
Step-by-Step Fix
1. Identify impurity sources
Look for unsafePerformIO, unsafeInterleaveIO, IORef, MVar, IOArray, FFI calls, System.Random outside IO, getCurrentTime, and readFile in pure contexts.
2. Isolate the impurity
Move impure operations to explicit IO contexts. Keep a thin impure layer at the edges (main, handlers) and pass data inward through pure functions.
3. Thread state explicitly
Instead of global mutable state, pass state as a parameter and return the updated state. Use State monad for implicit threading.
4. Use dependency injection
Pass configuration, random seeds, and other dependencies as explicit parameters. Use ReaderT for implicit dependency injection.
5. Test purity
A pure function is trivially testable: same input → same output. No mocking, no setup. If a test requires IO, the function is not pure.
Prevention Tips
- Never use
unsafePerformIOin production code meant to be pure - Keep IO at the application boundary (main, handlers, event loops)
- Pass all dependencies as explicit parameters
- Use
State/ReaderTfor managed state and configuration - Pure functions are easier to test, compose, and reason about
Common Mistakes with pure func
- Mixing let bindings with <- bindings in do notation, producing type errors
- Overlapping type class instances that cause GHC to reject the program with ambiguous dispatch errors
- Non-exhaustive pattern matches that compile with warnings then crash at runtime
These mistakes appear frequently in real-world HASKELL code. DodaTech's contributors have identified these patterns through analysis of open-source projects and production systems.
Practice Exercise
Write a pure function that safely divides two integers using Maybe, then test it with edge cases like division by zero and negative numbers.
This exercise reinforces the concepts covered in this guide. Try implementing it before checking online solutions.
Real-World Use Case
Haskell is widely used in fintech, blockchain, and compiler development. Companies like Standard Chartered, IOHK, and Facebook use Haskell for production systems where correctness and maintainability are critical. This pattern appears in real-world Haskell codebases including those powering the DodaTech infrastructure stack. Understanding pure func correctly helps prevent bugs in production systems and makes your HASKELL code more maintainable.
FAQ
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro