Most Haskell expressions are not supposed to perform I/O, unless by a
backdoor (please don't abuse it). But we recognize that debug printing is a
reasonable use of the backdoor.

The debug printing functions are in
Debug.Trace.
I only talk about trace, traceShow, traceShowId.
They cover most cases.

I will illustrate with this function:

f n | n <= 1 = n
| r == 0 = f q
| otherwise = f (3*n + 1)
where
(q, r) = n `divMod` 2

trace :: String -> a -> a

trace msg x equals x but also prints
msg. I use it to print out the cases hit:

import Debug.Trace
f n | n <= 1 = trace "base case" n
| r == 0 = trace "even case" (f q)
| otherwise = trace "odd case" (f (3*n + 1))
where
(q, r) = n `divMod` 2

Sample session:

prompt> f 10
even case
odd case
even case
even case
even case
even case
base case
1

If you want the messages to be more informative, e.g., also printing the
values of n, it can be done:

f n | n <= 1 = trace ("base case n=" ++ show n) n
| r == 0 = trace ("even case n=" ++ show n) (f q)
| otherwise = trace ("odd case n=" ++ show n) (f (3*n + 1))
where
(q, r) = n `divMod` 2

Sample session:

prompt> f 10
even case n=10
odd case n=5
even case n=16
even case n=8
even case n=4
even case n=2
base case n=1

traceShow :: Show a => a -> b -> b

traceShow n x equals x but also prints
n. I use it to print the values of n:

import Debug.Trace
f n | n <= 1 = traceShow n n
| r == 0 = traceShow n (f q)
| otherwise = traceShow n (f (3*n + 1))
where
(q, r) = n `divMod` 2

Sample session:

prompt> f 10
10
5
16
8
4
2
1
1

If I am tired of writing “traceShow n” 3 times (and later
deleting them thoroughly), here is a cool trick:

import Debug.Trace
f n | traceShow n False = undefined
f n | n <= 1 = n
| r == 0 = f q
| otherwise = f (3*n + 1)
where
(q, r) = n `divMod` 2

Explanation: A new case that the computer must check first, which will be
summarily rejected anyway, but it's the journey of printing n that matters.

Benefits: easy and unintrusive to add, and easy to delete later.

traceShowId :: Show a => a -> a

traceShowId x equals x and also prints it.
I use it to print the results of (q, r) = n `divMod` 2:

import Debug.Trace
f n | n <= 1 = n
| r == 0 = f q
| otherwise = f (3*n + 1)
where
(q, r) = traceShowId (n `divMod` 2)

Sample session:

prompt> f 10
(5,0)
(2,1)
(8,0)
(4,0)
(2,0)
(1,0)
1

This also shows an effect of lazy evaluation: when n=1, q and r are unused,
n `divMod` 2 is not evaluated, so there is no printing of (0,1).