Easiest way to debug/visualize recursive function calls in Haskell?

Viewed 421

I'm learning Haskell and I decided to implement this simple algorithm for doing part of the insert sort algorithm:

while j > 0 and A[j-1] > A[j]
    swap A[j] and A[j-1]
    j ← j - 1
end while

I did like this:

miniSort:: (Eq(a), Ord(a)) => Int -> [a] -> [a]
miniSort j list = if (list !! j) < (list !! (j-1)) && j >0
        then miniSort (j-1) (swapElements j (j-1) list) 
        else list

It was kinda hard to get it right but I did (I guess).

While in imperative programming languages to view each step I could simply do

while j > 0 and A[j-1] > A[j]
    print("j is $j")
    print("swapping $A[j] with $A[j-1]")
    swap A[j] and A[j-1]
    print("swapped list: $A")
    j ← j - 1
end while
print("ended with j $j")

On Haskell it's much much harder to insert logging functionality through the Writer Monad. I didn't even try because it would be a mess, and then when I wanted to clean the logging things it would be another mess.

Isn't there a way to view the function calls branching in Haskell?

For example:

miniSort 3 [1,2,4,3,7,8,5]

Would expand to something like this:

miniSort 3 [1,2,4,3,7,8,5] = if (3) < 4 && 3 >0
        then miniSort (2) [1,2,3,4,7,8,5]
miniSort 2 [1,2,3,4,7,8,5] = if (3) < 2 && 2 >0
        else [1,2,3,4,7,8,5]
[1,2,3,4,7,8,5]
3 Answers

As Fyodor Soikin said, trace inserts debug messages into arbitrary code, which will be printed when the statement you apply it to is evaluated.

import Debug.Trace
import Text.Printf

miniSort j list
 = if trace (printf "miniSort %i %s = if %i < %i && %i>0"
                              j (show list) (list!!j) (list!!(j-1)) j)
                      $ list !! j < list !! (j-1) && j>0
    then trace (printf "then miniSort %i %s" (j-1) (show list))
           miniSort (j-1) (swapElements j (j-1) list) 
    else traceShowId list

However, a couple of caveats:

  • If you find yourself wanting this for a simple algorithm like a sort, you're doing something wrong. Which indeed you are – it makes no sense to sort a list like that in Haskell. Specifically, indexing into lists is almost never a good idea: it's awkward, error-prone and slow.
    Good Haskell code generally uses pattern matching instead of indexing etc.. For example,

    type SortedList a = [a]
    
    insertion :: Ord a => a -> SortedList a -> SortedList a
    insertion n (x:xs)
     | n>x    = x : insertion n xs
    insertion n xs = n : xs
    
    insertSort :: Ord a => [a] -> SortedList a
    insertSort [] = []
    insertSort (x:xs) = insertion x $ insertSort xs
    

    See, no indices anywhere. Much less that can go wrong here.
    (Whether it makes any sense to implement insertion sort on Haskell lists in the first place is of course another matter!)

  • Because (non-monadic) Haskell doesn't really specify any evaluation order, trace can often come out in unexpected, possibly jumbled order. Really, only use it for debugging a single detail in the middle of a big, already-existing function. In general it's much better to instead refactor and unit-test thoroughly.
    And never use trace for actual logging!

  • Using the writer monad not only solves these problems, it also makes it easier (and much more reliable) to remove such statements again. For one thing, you don't really need to do that at all because you can just ignore the log data, use a polymorphic monad with dummy logging that will optimise it away from being generated in the first place, etc.. If you do remove the statements and un-writer the type, then the type checker will highlight any places where you forgot to do it.

Note: this solution is overkill and not suitable for beginners. For practical advice, follow leftaroundabout's answer.

We can define minisort as a monadic function, but one which is polymorphic over the monad. Also, we'll define it using "open recursion", which means that the function receives its own recursion step as a parameter, instead of directly calling itself. This opens a seam into which we'll be able to insert instrumentation later:

{-# LANGUAGE ScopedTypeVariables #-}

-- some helper type synonyms
type Minisort m a = Int -> [a] -> m [a]
type Open f = f -> f

minisortAux :: (Eq a, Ord a, Monad m) => Open (Minisort m a)
minisortAux recurse j list =
    if (list !! j) < (list !! (j-1)) && j >0
        then recurse (j-1) (swapElements j (j-1) list) 
        else pure list

We can recover the original minisort by choosing the no-op Identity as the monad, and "closing" the recursion using Data.Function.fix:

import Control.Monad.Identity

minisort :: (Eq a, Ord a) => Int -> [a] -> [a]
minisort j list = runIdentity $ fix minisortAux j list

But we can also instrument the function so that it works in IO and prints its parameters at each iteration:

-- An Instrumentation transforms an open function into
-- another open function with extra behaviour.
-- Notice that instrumentations of the same type can be composed!
type Instrumentation f = Open f -> Open f

minisortIO :: forall a. (Eq a, Ord a, Show a) => Minisort IO a
minisortIO j list = fix (instrument minisortAux) j list 
  where
    instrument :: Instrumentation (Minisort IO a)
    instrument openFunction recurse j list = 
        do print $ "starting call with params " ++ show j ++ "  " ++ show list
           r <- openFunction recurse j list
           print $ "ending call with value" ++ show r
           return r

Or we could accumulate the parameters using Writer:

import Control.Monad.Writer

minisortWriter :: forall a. (Eq a, Ord a) => Minisort (Writer [(Int,[a])]) a
minisortWriter j list = fix (instrument minisortAux) j list 
  where
    instrument :: Instrumentation (Minisort (Writer [(Int,[a])]) a)
    instrument openFunction recurse j list = 
        do tell [(j,list)]
           openFunction recurse j list

Besides printing or logging stuff, we could do things like asking the user mid-recursion for the value of a parameter, or even let the user enter some result value for a recursive call and avoid doing the call altogether.

Off-topic, but you would be better off learning to write idiomatic Haskell code than trying to shoe-horn an impure, iterative algorithm into Haskell.

-- insert a value into a sorted list, preserving the sort
insert :: Ord a => [a] -> a -> [a]
insert [] y = [y]
insert (x:xs) y | x < y = x : insert xs y
                | otherwise = y : x : xs


-- sort a list by repeated inserting the first item of the list
-- into its proper place in the sorted remnant.
insSort :: Ord a => [a] -> [a]
insSort [] = []
insSort (x:xs) = insert (insSort xs) x

The whole business with swapping is just how you would implement insert in a more imperative language; it's not how you would do it in Haskell.

Related