Dynamic programming with Project Euler #18
Your code is correct, fast, and I'd like to emphasize that you thoroughly include types for your functions, which greatly helps readability.

While arrays offer fast performance, I'd say that in this case operations with indexes and bounds of the arrays cloud the main idea considerably. Therefore I'd offer to use lists and functions native to lists that avoid indexing (the same could be also achieved with arrays, by writing similar functions for them).

The main idea, computing the maximum path lengths for one row, can be then nicely described by zipping:

-- OPT(r,i) = v_ri + max(OPT(r+1,i), OPT(r+1,i+1))
opt :: [Int] -- ^ the current row
    -&gt; [Int] -- ^ the maximum paths from the previous row
    -&gt; [Int]
opt row mxs = zipWith (+) row $ zipWith max mxs (tail mxs)


Then, computing the maximum path can be expressed as folding opt over a list of lists that represents a triangle:

-- Given a triangle (the first row containing `n` elements, the last row 1
-- element), compute the maximum path.
max_path :: [[Int]] -&gt; Int
max_path = head . foldr opt (repeat 0)


where we start with an infinite list of zeros. This is safe, as we always only access a finite number of elements of this starting list.

While the algorithm remains the same, avoiding explicit indexing makes it more readable.



Minor notes:


it's customary to write f . g $ x instead of f $ g $ x to emphasize function composition
putStrLn . show can be replaced by just print


(I suggest you to try out hlint, which gives many interesting hints how to improve a Haskell program, including the two above.)