Debugging without a “real” debugger
Tags: haskell, purescript, functional, productivity
Published: 2023-07-27 15:15:00

📹 Hate reading articles? Check out [the complementary video](https://youtu.be/iAzaShuVpPc), which covers the same content.

---

First, a confession. I don’t think I’ve ever found a bug using a “real” debugger. By now, I’ve probably found (and fixed) hundreds of bugs, mostly by reading code and printing them to the console like a caveman. I have years of experience working on top of JVM, and the same with JavaScript — I had access to all the wonderful tools both platforms provide. It’s not my cup of tea.

If you find adding markers, stepping over, or stepping into your code fun or useful, you do you. Did you know that GHC has [the GHCi Debugger](https://downloads.haskell.org/ghc/latest/docs/users_guide/ghci.html#ghci-debugger)?

But if you wonder how to debug in Haskell without a debugger, this tutorial is for you. Especially if someone convinced you that “Haskell is pure, so you can’t print anything”.

> 🐞 Note: It also applies to PureScript, and I’ll include a couple of snippets.

## This program 🐞 you

Here is a simple program we are going to debug:

```haskell
program :: IO ()
program = do
  rawJSON <- fetchData -- [1]
  let items = decodeItems rawJSON -- [2]
  let result = keepInteresting items -- [3]
  report result -- [4]

-- fetchData :: IO ByteString
-- decodeItems :: ByteString -> [Item]
-- keepInteresting :: [Item] -> [Text]
-- report :: [Text] -> IO ()
```

- [1] We fetch some raw data.
- [2] Decode it.
- [3] Process it.
- [4] Report the result.

> 🐞 We’ll (re)investigate a genuine bug I made while trying to create an example for this tutorial.

I mocked some data, provided basic implementations (we’ll see them later), etc. I expected running the program would print at least one result back, but surprisingly (and luckily), I got an empty result. I messed up. Debugging time!

### print “Hello, world”

There is no random global state we have to worry about in Haskell. Functions only depend on their inputs. 

`program` takes no inputs (parameters). So, I messed up somewhere “inside” the function, specifically in one of the used functions. Because there 4 steps, there are 4 major places for the bug to hide. We can keep narrowing down the scope.

> 🐞 Of course, there are more than “just” 4 places, but the program is so tiny, and the types are strong enough that it’s hard to mess up otherwise.

We can use `putStrLn` to check which step fails or produces unexpected results:

```haskell
program :: IO ()
program = do
  rawJSON <- fetchData
  putStrLn $ "Step 1: " ++ show rawJSON
  let items = decodeItems rawJSON
  putStrLn $ "Step 2: " ++ show items
  let result = keepInteresting items
  putStrLn $ "Step 3: " ++ show result
  report result
```

Let’s see the output:

```
Step 1: "[ { \"label\": \"Box\", \"score\": 2 }, { \"label\": \"Big box\", \"score\": 12 } ] }"
Step 2: []
Step 3: []
"Result: []"
```

When I saw this, I thought: “Okay, step 1 looks okay, but then somewhere in step 2, we lost all the data”. Step 2 is the `decodeItems` function. Let’s zoom in on it:

```haskell
decodeItems :: ByteString -> [Item]
decodeItems raw = fromRight [] $ eitherDecode raw
```

I was trying to cut the corners and default to an empty list in case of any errors (because I didn’t expect any errors). Wouldn’t it be nice to see these errors now? Also, note that the function doesn’t return an `IO`.

I know. The first snippet was unfairly easy to debug because we can just `putStrLn` things when we use IO. But we don’t always have this luxury in Haskell:

```haskell
decodeItems :: ByteString -> [Item]
decodeItems raw = do
  let tmp = eitherDecode raw
  putStrLn $ "TMP: " ++ show tmp -- compilation error
  fromRight [] tmp
```

We get a compilation error — we can’t mix `IO` and not `IO` expressions like that. What do we do?

In this simple case, we can change the type signature (to `IO [Item]`), rewrite the function, add `putStrLn`, adopt callers to the new result type, find the bug, and eventually roll everything back... This works. But this is super annoying. Especially if you have more than a few nested functions and not oneliners.

This is where [Debug.Trace](https://hackage.haskell.org/package/base-4.18.0.0/docs/Debug-Trace.html) comes in.

### trace “pls hlp”

`Debug.Trace` is part of the `base` (standard library) and provides several functions for printing values.

For instance, we can use [`traceShowId`](https://hackage.haskell.org/package/base-4.18.0.0/docs/Debug-Trace.html#v:traceShowId) to print the result of decoding:

```haskell
decodeItems :: ByteString -> [Item]
decodeItems raw = fromRight [] $ traceShowId $ eitherDecode raw
```

When we run this, we see a new trace in the log:

```
...
Left "Error in $: endOfInput at '}'"
...
```

The function shows and returns the value: `traceShowId :: Show a => a -> a`, so we can seamlessly plug it in most places (given that a value is `Show`able). 

The only issue is that we’re printing the value without any label or indication, which can easily be lost among all the other output. It would be nice if we could add a searchable tag or meaningful prefix like `“!!!!!!!!!!!!DEBUG!!!!!!!!!!!!!!!!!!!”`.

The `Trace` module provides other alternatives. For example, we can use [`trace`](https://hackage.haskell.org/package/base-4.18.0.0/docs/Debug-Trace.html#v:trace) to make a custom message:

```haskell
decodeItems :: ByteString -> [Item]
decodeItems raw = 
  let tmp = eitherDecode raw
   in trace ("HLP: " ++ show tmp) $ fromRight [] tmp
```

This time, we acquire a searchable label:

```
HELP: Left "Error in ..."
```

This version’s output is easier to read and find but requires moving some code around.

If you’re from the future or lucky enough to use the latest ghc, you can use `traceWith` to get the best of both worlds:

```haskell
decodeItems :: ByteString -> [Item]
decodeItems raw = 
  fromRight [] $ traceWith (\a -> "AAA: " ++ show a) $ eitherDecode raw
```

### [PureScript] spy “dupa”

In other words, if you’re using PureScript, [`spy`](https://pursuit.purescript.org/packages/purescript-debug/6.0.2/docs/Debug#v:spy) is the only function you need – it prints and returns the value but also takes an additional label:

```haskell
decodeItems :: String -> Array Item
decodeItems raw = fromRight [] $ spy "HOPE IT WORKS" $ readJSON raw
```

> 🐞 We need to `import Debug (spy)` from [purescript-debug](https://pursuit.purescript.org/packages/purescript-debug/6.0.2/docs/Debug).

When we run this, we see the label and the value:

```
...
HOPE IT WORKS: Left { ... }
...
```

As a bonus, `spy` has a custom usage warning, so we remember not to push this into prod.

```haskell
spy :: forall a. DebugWarning => String -> a -> a
```

## Next steps

So, where is the bug? 

Here is the full decoding error: `Left "Error in $: endOfInput at '}'"`

The bug didn’t hide in `decodeItems`; it received corrupted data from its input — the output of `fetchData`. ~~We~~ I just didn’t notice it. Here is the raw data:

```haskell
fetchData :: IO ByteString
fetchData = pure "[ { \"label\": \"Box\", \"score\": 2 }, { \"label\": \"Big box\", \"score\": 12 } ] }"
```

It’s a broken json. I left an extra curly bracket at the end. We’re trying to decode a list of items, but we can’t.

---

This time, after narrowing down the scope once or twice, it was easy to spot the bug by reviewing the code. It was also easy to run everything. What if it wasn’t easy? What if we had a large app with more hoops to jump through?

We don’t have to struggle debugging functions somewhere in the middle of the production service — **functions only depend on their inputs**. We can find the problematic arguments and test the function in isolation.

### Use REPL

If you want a more straightforward and faster feedback loop, you can use `ghci` (or GHCi), an interactive environment (aka REPL). How to use it is out of the scope of this tutorial and depends on personal preferences, but the rough idea is this:

- Find the problematic function (by running the whole app or specific functions using ghci).
- Modify the code (for example, to add some prints or traces).
- Find the problematic inputs.
- Repeat with a narrower scope.

> 🐞 As I mentioned in the intro, GHCi comes with a debugger.

### Write tests

Alternatively, if you like to write tests, you can follow a similar process as with ghci, but instead of reproducing the problem by running functions with specific inputs in the REPL, paste them into the unit (or property-based) tests. As a bonus, you can keep the tests to catch future regressions.

## EOF

For me, debugging in Haskell comes down to using prints and traces (or proper production observability — actual logging and tracing) to narrow down the scope of the bug and using tests or ghci to reproduce and fix the issues.

In the example, we started from `program`, found the problematic function using prints, stepped into it, found an issue using tracing, and the rest is history.

### Appendix I. The rest of the implementations

```haskell
keepInteresting :: [Item] -> [Text]
keepInteresting = map label . filter ((>) 10 . score)

report :: [Text] -> IO ()
report items = print $ "Result: " <> show items

data Item = Item {label :: Text, score :: Int} deriving (Show)

$(deriveJSON defaultOptions ''Item)
```

--- 

{% embed https://youtu.be/iAzaShuVpPc %}