raco pkg install debug
It's the simplest tools that we reach for first. That's why one of the most common ways programmers debug their programs is with printf
, display
, print
, console.log
, System.out.println
, System.Console.WriteLine
, or whatever other console printing tool your language of choice has. It's easy to understand, not too inflexible, and usually good enough. But even for only simple debugging, is that the best a language can do?
Not in Racket! The debug
package (source) by Alex Knauth is a unique approach to debugging. Instead of manually writing print statements and formatting strings in the middle of the flow of your code, debug
only requires two characters:
#lang debug racket
(* #R(+ 1 2 3) (- 10 5))
Those two characters are #R
; using #lang debug racket
adds some magic to the Racket where any expressions that look like #R(...)
have their result printed to current-error-port
(which defaults to stderr) when evaluated. The above program produces this error output:
(+ 1 2 3) = 6
There are a few important details to note. Firstly, the #R(...) expression only captures what the source code looks like and the final output value, it does not display what the inputs evaluated too. This is in general difficult to do because it's not easy to tell if the surrounded expression is a macro or a function application. As an example, when used like this:
(define some-var 1)
(* #R(+ some-var 2 3) (- 10 5))
The error output is (+ some-var 2 3) = 6
. The information that some-var
evaluated to 1 is not available for debugging.
It's also somewhat verbose to log a non-expression, for instance a variable. To log only some-var
in the previous example, values
must be used:
(define some-var 1)
(* (+ #R(values some-var) 2 3) (- 10 5))
All that said, this is still easier than inserting printf
statements everywhere, as statements are tricker to interleave into complex expressions. You'd have to pull out the expression you want to log and name it with a let
or define
before printing it.
debug
is an example of a meta language. Rather than being its own #lang debug
language, it's defined as a language that re-uses all the definitions and syntax of some base language and adds its own special reader extensions. You can just as easily use debug
with typed/racket
code:
#lang debug typed/racket
(* #R(+ 1 2 3) (- 10 5))
Meta languages are powerful tools, and are best suited for specifying "this adds some custom syntax for reading the source file but doesn't change anything else". Another example of a meta language is at-exp
, which adds Scribble's @-expressions to the underlying language:
#lang at-exp racket
@+[1 2 3]
@displayln{Hello world!}
Most meta languages can be freely mixed with each other:
#lang debug at-exp racket
@+[1 #R@*[10 5]]
But back to debug
. There's a few other features available as well. If you use #RR
instead of just #R
, the output includes a line number:
#lang debug racket
(+ 1 #RR(* 10 5))
output:
(* 10 5) = 50 on line 2
51
Adding another R to get #RRR
includes the filename as well:
#lang debug racket
(+ 1 #RRR(* 10 5))
output:
(* 10 5) = 50 on line 2 in "/.../debug-example.rkt"
51
Additionally, since all logs are sent to current-error-port
, you can change where they're sent by parameterizing the current error port. This lets you, for example, write all these logs to disk or send them over a network. But 99% of the time, all you need is a better printf
. For more complex debugging, there are some more featureful choices available. The medic
package lets you instrument much more comprehensive full-program debugging overlays, while the unstable/debug
library that ships with standard Racket includes some debug
forms that operate similarly to this package with more verbosity.
So, next time you're tempted to debug with printf
, go meta and add debug
to your language!
Note: needs exposition on when more sophisticated debugging tools like Medic are appropriate