How does one write efficient Dynamic Programming algorithms in Haskell?
Data Structures & Algorithms practice on Codemia
Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.
Writing Efficient Dynamic Programming Algorithms in Haskell
Dynamic programming (DP) relies on two properties: optimal substructure and overlapping subproblems. In imperative languages, DP typically means filling in a mutable array in a specific order. Haskell does not have mutable variables by default, so implementing DP efficiently requires a different mindset. The good news is that Haskell's lazy evaluation provides a natural mechanism for memoization, and the language offers several array types that support O(1) indexing.
This article covers four practical techniques for DP in Haskell, progressing from the simplest lazy-list approach to high-performance mutable arrays in the ST monad.
Technique 1: Lazy List Memoization
Haskell's laziness means that values in a data structure are not computed until they are needed, and once computed, they are cached. You can exploit this by defining a list (or other lazy structure) where each element depends on previously computed elements:
This works because Haskell evaluates each element of fibs at most once. The list acts as a memo table. However, list indexing with !! is O(n), so this approach is only suitable for problems where you access elements sequentially or need only the last few values.
For the classic 1D DP problem of counting stair-climbing ways (you can take 1 or 2 steps at a time):
Technique 2: Immutable Array Memoization
For O(1) random access, use Data.Array. You can define an array where each element's value depends on other elements in the same array. Haskell's laziness ensures elements are computed on demand:
This pattern generalizes to any DP problem. Here is the classic 0/1 knapsack:
The array bounds define the full DP table, and each cell references other cells via the table array. Lazy evaluation ensures no cell is computed more than once.
Technique 3: Mutable Arrays with the ST Monad
For the best performance, especially on large DP tables, use mutable arrays inside the ST monad. This gives you the same fill-in-order style as imperative DP while keeping the overall computation pure:
STUArray is an unboxed mutable array, which stores values without pointer indirection and is significantly faster than boxed arrays for numeric types. The runST function ensures the mutable state does not escape, so the function remains pure from the caller's perspective.
Technique 4: Using Data.Map for Sparse DP
When the DP state space is large but only a small fraction of states are actually visited (sparse problems), using Data.Map is more memory-efficient than allocating a full array:
This approach threads the memo map through the recursion. While it works, the explicit state threading is verbose. For cleaner code, consider using the MemoTrie library or Data.MemoTable for automatic memoization.
Choosing the Right Technique
The decision depends on the problem characteristics:
Common Pitfalls
- Using lists for random access. List indexing with
!!is O(n) per lookup. For a 2D DP table of size N x M, this turns an O(NM) algorithm into O(N^2 * M^2). Always use arrays when random access is needed. - Space leaks from laziness. Lazy evaluation can accumulate unevaluated thunks that consume far more memory than the actual values. Use
seqor strict arrays (Data.Array.Unboxed,Data.Map.Strict) to force evaluation and avoid space leaks. - Boxed vs unboxed arrays.
Data.Arraystores elements as pointers to heap objects. For numeric DP, this pointer indirection slows things down considerably. UseData.Array.Unboxed(for immutable) orData.Array.STwithSTUArray(for mutable) to store values inline. - Incorrect bounds in self-referential arrays. If an array element references an index outside the declared bounds, you get a runtime error. Double-check your base cases and boundary conditions.
- Forgetting strictness in Map-based memoization. Using
Data.Map.Lazywith numeric values builds up thunks for every inserted value. UseData.Map.Strictto evaluate values at insertion time.
Summary
Haskell provides several effective approaches for dynamic programming. Lazy list memoization is the simplest but only works well for sequential access. Immutable arrays from Data.Array offer O(1) lookup with a clean self-referential definition that leverages lazy evaluation for automatic memoization. For maximum performance, mutable unboxed arrays in the ST monad match imperative DP speeds while remaining pure. Sparse problems benefit from Data.Map.Strict to avoid allocating the full state space. The most important considerations are choosing the right data structure for your access pattern and being vigilant about space leaks caused by unevaluated thunks.
Related reading
- How does Paxos handle packet loss and new node joining?
- How does Python's cmp_to_key function work?
- How does Radix Sort work?
- How does Raft deals with delayed replies in AppendEntries RPC?
- How does setting baselineAligned to false improve performance in LinearLayout?
- How does sorting a string in an array of strings and then sorting that array come out to be Oaslogalogs?
- How does Raft guarantee log consistency?
- How does raft preserve safty when a leader commits a log entry and crashes before informing followers this commitment?

DSA Fundamentals
Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.
View the courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
Data Structures & Algorithms practice on Codemia
Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.