6

A similar question was asked here but is over ten years old and I hope that some progress has been made in the meantime that gives rise to different answers now.

Right now I am using VSCode and installed the "Haskell GHCi Debug Adapter Phoityne". It works but due to the laziness of Haskell (and the separation of pure and IO-code and the functional nature of Haskell), it is still hard to debug the code.

Some solutions to such problems seem to have been proposed. For example, I found this article that compares debugging techniques in Haskell and names several packages that try to improve the experience by introducing "oberservations" of functions. Among other things, Hood, Hoed and Hat are named and I additionally found debug on hackage.

However, unfortunately, I do not get any of them to work. When installing Hood, I get the error message that it has the dependency FPretty that isn't compatible with newer versions of the base package (this is already filed as an issue since 2018). The same issue arises for Hoed. When installing Hat, I get many many errors starting with a complaint about missing modules and debug crashes upon interpretation of the program.

As a result, I was not able to test any of those. However, what I'd really like is a graphical debugger (preferably an improved version of the one already implemented in VSCode!) that jumps to the source code line under inspection and then displays a small window that unfolds the recursion step by step (either without evaluation or, even better, with some option for a step-by-step evaluation after the recursion is completed, i.e. just before actual evaluation), similarly to how the effect of foldr is made visible in this wiki article.

Does one of the above packages do this? And wouldn't it be nice for the Haskell community to have such a functionality for VSCode to improve the ecosystem and attract newcomers? Or is this already possible and I just didn't know how to configure Phoityne correctly? Or do you have entirely different ways to debug Haskell efficiently? What is your workflow?

exchange
  • 363
  • 4
  • 9
  • 1
    While I agree that his could be quite helpful to newbies, it would hardly be effective at debugging real-world scale projects. IMO the best way to debug is to _not_ debug, but just refactor and unit-test so well that there's never a need to _step through_ any code. Haskell's strong types help enourmously with this. – leftaroundabout Aug 23 '21 at 15:05
  • Isn't it usually said that debugging becomes necessary precisely when code becomes more complicated due to bigger ("real world") problems? I thought that's what debuggers were invented for in the first place. And debugging can not only be helpful for finding errors in code but also for observing how values change without the need to print everything by hand (which would be especially cumbersome in Haskell due to the IO monad). – exchange Aug 23 '21 at 15:55
  • Well, debugging becomes necessary for lots of reasons. Several of them are avoided entirely in Haskell (e.g. values _don't_ change, ever, so there's also no need to observe how they not do it). Complexity brings up both likelyhood of bugs and the difficulty of dealing with them, yes – but the best solution is not to make the debugger more complicated in an arms race, but instead to refactor the code into less complex components that can easily be tested and debugged without special tooling. (But, _printing by hand_ is ocasionally still useful, not in the IO monad but with `Debug.Trace`.) – leftaroundabout Aug 23 '21 at 16:18
  • Yes, I agree that refactoring code is helpful and important. However, I do not agree that this makes debuggers obsolete. Consider for instance the code: `foldr (\x y -> f y) z [0...n]`. This is a kind of loop where `f` is applied `n` times to its previous output, starting with `z`. This output *does change* at each iteration. Thus, in this specific sense, Haskell does implement mutation of variables, though in a clever way, using recursion. And it would definitely be useful to keep track of those changes of input and output variables of recursive functions - which a debugger could help to do. – exchange Aug 23 '21 at 17:26
  • (At least when those recursive calls eventually are evaluated. In those cases, the debugger could provide a step by step evaluation of the final expression that must be evaulated.) – exchange Aug 23 '21 at 17:33
  • 1
    Well, what would be really cool is a tool that displays already-evaluated data structures, and represents thunks by their source code that can then be stepped into by demand. But that's quite different from what we would normally consider as a debugger. The imperative notion of _steps_ just doesn't really apply to Haskell – evaluation order is usually all over the place, because it's not defined at all (except with `seq`) by the language. – leftaroundabout Aug 23 '21 at 17:44
  • Yes, okay, I agree that it would be different from a conventional debugger but that seems to be what the nature of Haskell requires. And yes, it would be really cool. – exchange Aug 23 '21 at 17:47
  • 1
    Nowhere as convenient or ergonomic as having a debugger—which I agree would we great to have—but to trace the execution of a recursive function defined by ourselves, one solution is writing it in an "open recursive" style and manually add some instrumentation: https://stackoverflow.com/questions/67782579/easiest-way-to-debug-visualize-recursive-function-calls-in-haskell/67787958#67787958 – danidiaz Aug 24 '21 at 09:04

1 Answers1

2

I never managed to get the existing debugging solutions working so I just use custom ghci commands and :set stop :status to have a nice display of variables and other useful info at each breakpoint stop.

In ghci:
:br someFunction - set breakpoint at someFunction
:br SomeModule.someFunction - set breakpoint at function in module
:br SomeModule 150 - set breakpoint at specific line
:stl - step local (step over)
:st - step (step in)
:status - display status

Contents of ~/.ghci:

-- Fancy debugging
_nl = "putStrLn \"\"\n"
_history = "putStrLn \"HISTORY:\"\n :history \n"
_context = "putStrLn \"CONTEXT:\"\n :sh context\n"
_breaks = "putStrLn \"BREAKS:\"\n :sh breaks\n"
_bindings = "putStrLn \"BINDINGS:\"\n :sh bindings\n"
_list = "putStrLn \"LIST:\"\n :list\n"
_status = \_ -> return $ ":!clear\n " ++ _breaks ++ _nl ++ _history ++ _nl ++ _bindings ++ _nl ++ _context ++ _nl ++ _list ++ _nl

-- Useful commands
:def hoogle \x -> return $ ":!hoogle \"" ++ x ++ "\""
:def hinfo \x -> return $ ":!hoogle --info \"" ++ x ++ "\""
:def stl \x -> return $ ":steplocal " ++ x
:def status _status
:def fprint \x -> return $ ":force " ++ x ++ "\n" ++ "print " ++ x
:def fpprint \x -> return $ ":force " ++ x ++ "\n" ++ "pPrint " ++ x

-- Show status on breakpoint
:set stop :status

Beside that there's also haskell-language-server and ghcid that are useful for faster error and type checking.

  • Thanks a lot for the answer. Could you provide some more info on how to use the .haskline file? Is ghci able to access the commands defined in it whenever the file is in the root folder or does one have to load it or install a package for loading it automatically? – exchange Jan 15 '23 at 11:20
  • 1
    @exchange Sorry I said `~/.haskeline`, I meant `~/.ghci` (ghci config file). Haskeline is just for the ghci history and other minor settings. https://mpickering.github.io/ghc-docs/build-html/users_guide/ghci.html#the-ghci-and-haskeline-files – Vito Canadi Jan 16 '23 at 12:39