Table of contents:
The simplest way to debug programs is to follow the function calls, by ``tracing'' the faulty function:
#let rec fib x = if x <= 1 then 1 else fib (x - 1) + fib (x - 2);; fib : int -> int = <fun> #trace "fib";; The function fib is now traced. - : unit = () #fib 3;; fib <-- 3 fib <-- 1 fib --> 1 fib <-- 2 fib <-- 0 fib --> 1 fib <-- 1 fib --> 1 fib --> 2 fib --> 3 - : int = 3 #untrace "fib";; The function fib is no longer traced. - : unit = ()
The difficulty here, is that the output of the trace system is not very informative in case of polymorphic arguments and/or results. Consider a sorting algorithm (say bubble sort):
let exchange i j v = let aux = v.(i) in v.(i) <- v.(j); v.(j) <- aux;; exchange : int -> int -> 'a vect -> unit = <fun> let one_pass_vect fin v = for j = 1 to fin do if v.(j - 1) > v.(j) then exchange (j - 1) j v done;; one_pass_vect : int -> 'a vect -> unit = <fun> let bubble_sort_vect v = for i = vect_length v - 1 downto 0 do one_pass_vect i v done;; bubble_sort_vect : 'a vect -> unit = <fun> #trace "one_pass_vect";; The function one_pass_vect is now traced. - : unit = () #let q = [| 18; 3; 1 |];; q : int vect = [|18; 3; 1|] #bubble_sort_vect q;; one_pass_vect <-- 2 one_pass_vect --> <fun> one_pass_vect* <-- [|<poly>; <poly>; <poly>|] one_pass_vect* --> () one_pass_vect <-- 1 one_pass_vect --> <fun> one_pass_vect* <-- [|<poly>; <poly>; <poly>|] one_pass_vect* --> () one_pass_vect <-- 0 one_pass_vect --> <fun> one_pass_vect* <-- [|<poly>; <poly>; <poly>|] one_pass_vect* --> () - : unit = ()
The function one_pass_vect
being polymorphic, its
vector argument is printed as a vector containing polymorphic values,
[|<poly>; <poly>; <poly>|]
, and thus we
cannot properly follow the computation.
A simple way to overcome this problem is to define a monomorphic
version of the faulty function. This is fairly easy using a type
constraint. Generally speaking, this allows a proper understanding
of the error in the definition of the polymorphic function. Once this
has been corrected, you just have to suppress the type constraint to
revert to a polymorphic version of the function. For our sorting
routine, a single type constraint on the argument of the
exchange
function warranties a monomorphic typing, that
allows a proper trace of function calls:
let exchange i j (v : int vect) = [...] exchange : int -> int -> int vect -> unit = <fun> [...] one_pass_vect : int -> int vect -> unit = <fun> [...] bubble_sort_vect : int vect -> unit = <fun> #trace "one_pass_vect";; The function one_pass_vect is now traced. - : unit = () #let q = [| 18; 3; 1 |];; q : int vect = [|18; 3; 1|] #bubble_sort_vect q;; one_pass_vect <-- 2 one_pass_vect --> <fun> one_pass_vect* <-- [|18; 3; 1|] one_pass_vect* --> () one_pass_vect <-- 1 one_pass_vect --> <fun> one_pass_vect* <-- [|3; 1; 18|] one_pass_vect* --> () one_pass_vect <-- 0 one_pass_vect --> <fun> one_pass_vect* <-- [|1; 3; 18|] one_pass_vect* --> () - : unit = ()
Predefined functions are traceable as any other function, if we
omit the problems related to polymorphism of the preceding section,
but predefined functions exhibit a new phenomenon: the user shares
these functions with the interactive system. This implies that every
call to the function that the interactive system may do, are now
traced as if they were yours.
This may be surprising:
#trace "map";; The function map is now traced. #[];; map <-- <fun> map --> <fun> map* <-- [<poly>] map* --> [<poly>] - : 'a list = []
The map
function is used by the interactive system
during the compilation of the phrase (syntax analysis, type checking,
compilation, linking and evaluation of the input Caml phrase). If you
don't need this behavior, you must define your own version of the
predefined function, using a dummy new definition, and trace this new
function equivalent to the predefined one:
#untrace"map";; map <-- <fun> map --> <fun> map* <-- [<poly>] map* --> [<poly>] The function map is no longer traced. - : unit = () #let map f l = map f l;; map : ('a -> 'b) -> 'a list -> 'b list = <fun> #trace "map";; The function map is now traced. - : unit = () #[];; - : 'a list = [] #map succ [1];; map <-- <fun> map --> <fun> map* <-- [<poly>] map* --> [<poly>] - : int list = [2]
This is a subtle particular case of the preceding phenomenon: if
you trace one of the predefined functions used during the
trace, the trace mechanism loops. This appears for instance if you
try to trace one of the printing functions defined in the format
pretty-printer, that the
trace uses to output values. If you really need to trace these
functions you must use the same trick as explained in the preceding
paragraph: redefine the functions before any use in your program, then
trace the new versions.
To keep track of assignments to data structures and mutable variables in a program, the trace facility is not powerful enough. You need an extra mechanism to stop the program in any place and ask for internal values: that is a symbolic debugger with its stepping feature.
Stepping a functional program has a meaning which is a bit weird to define and understand. Let me say that we use the notion of runtime events that happen for instance when a parameter is passed to a function or when entering a pattern matching, or selecting a clause in a pattern matching. Computation progress is taken into account by these events, independently of the instructions executed on the hardware.
Although this is difficult to implement, there exists such a debugger
for Caml under Unix: this is the camldebug
user contribution.
Unfortunately its implementation uses advanced features of Unix, hence
it is not directly portable under Macintosh or PC.
On these machines, you must use the trace or explicit messages
inserted here and there into the program.
In fact, for complex programs, it is likely the case that the programmer will use explicit printing to find the bugs, since this methodology allows the reduction of the trace material: only useful data are printed and special purpose formats are more suited to get the relevant information, than what can be output automatically by the generic pretty-printer used by the trace mechanism.
Contact the author Pierre.Weis@inria.fr