Get the full source, or continue reading for a better understanding of it. The source below is slightly changed for formatting reasons.

module Main (main) where

import Data.Array.IO
import Data.Array.Unboxed
import Data.Char
import System.Environment (getArgs)
import Control.Monad.State

type BFMonad a = StateT BFState IO a
data BFState = BF {
    pc, ptr :: Int,
    mem     :: IOUArray Int Int,
    program :: UArray Int Char
}

General imports and the two main data structures, a state monad transformer over IO, and the state type, which holds the program counter, a pointer, the memory as a mutable array and the program code as an immutable array.

-- Entrypoint
main :: IO ()
main = getArgs >>= \a -> unless (null a) $ readFile (head a) >>= runBF

runBF :: String -> IO ()
runBF pstr = do
    arr <- newArray (0,30000) 0
    evalStateT evalBF (BF 0 0 arr progArr)
  where
    progArr = listArray (0,length pstr) $ pstr ++ "@"

The main function reads a file (if given on cmdline) and passes it on to runBF. runBF allocates an array of 30 000 and runs the program in that array. Note that 30 000 is a sort of arbitrary number, and since Brainfuck essentially models a Turing machine, the tape is in theory infinite.

evalBF :: BFMonad ()
evalBF = do
    c <- fetch
    unless (c == '@') $ run c >> alterpc (+1) >> evalBF
  where
    run c = case c of
        '>' -> alterptr (+1)
        '<' -> alterptr (subtract 1)
        '+' -> alterptrval (+1)
        '-' -> alterptrval (subtract 1)
        '.' -> getptrval >>= lift . putChar . chr
        ',' -> lift getChar >>= alterptrval . const . ord
        '[' -> while True 
        ']' -> while False
        _   -> return () --Invalid commands are ignored.

Well, evalBF is a fetch-execute loop. There are 8 different commands available. The @ command is not as standard as the rest, but it is common for Brainfuck interpreters to see it as an exit command. See below for the functions that are called above.

-- Utility functions

fetch :: BFMonad Char
fetch = liftM2 (!) (gets program) $ gets pc

alterpc :: (Int -> Int) -> BFMonad ()
alterpc f = modify $ \s -> s { pc = f $ pc s }

alterptr :: (Int -> Int) -> BFMonad ()
alterptr f = modify $ \s -> s { ptr = f $ ptr s }

getptrval :: BFMonad Int
getptrval = gets mem >>= \a -> gets ptr >>= lift . readArray a

alterptrval :: (Int -> Int) -> BFMonad ()
alterptrval f = do
    m <- gets mem
    p <- gets ptr 
    getptrval >>= lift . writeArray m p . f

while :: Bool -> BFMonad ()
while b = do
    v <- getptrval
    unless (if b then v /= 0 else v == 0) $ jump 0
  where
    jump :: Int -> BFMonad ()
    jump i = do
        c <- alterpc (if b then (+1) else subtract 1) >> fetch
        case c of
            '[' -> if not b then unless (i==0) $ jump (i-1) 
                            else jump (i+1)
            ']' -> if b     then unless (i==0) $ jump (i-1)
                            else jump (i+1)
            _   -> jump i