Interactive development features are mostly found in dynamically-typed interpreted programming languages like Python or JavaScript. While OCaml is a statically-typed compiled language, it is still possible to program in an interactive style using a REPL. However, OCaml will never be quite as flexible and interactive as something like Lisp because of its greatest feature: the strong static type system.
Testing functions using the REPL
One of the nicest features of OCaml is that is has both a byte-code compiler
(ocamlc
) and a native-code compiler (ocamlopt
). This means that you can
develop programs in an interactive, bottom-up style using the REPL. Bottom-up
development is a technique most-often leveraged by Lisp programmers in which you
can write a single function, compile it and send it to the REPL, and then test
that function interactively in the REPL. OCaml’s fast bytecode compiler makes it
possible to use this technique that is usually unique to Lisps and interpreted
languages.
Sending code to the REPL in Emacs
I’ll describe the process for interactive development using Emacs which is my text editor of choice. Similar techniques should exist for other editors such as VS Code or Vim.
OCaml’s REPL is called utop
and it has a lot of nice features that make it
well-suited for interactive development. If you’re using Emacs, you can send
your OCaml code to utop
to be evaluated. Here’s an example of using utop
to
test a single function.
open Base
let sum_list list = List.fold ~f:( + ) ~init:0 list
To send this code to utop
, highlight it and press C-x C-r
(or M-x utop-eval-region RET
). You can even send an entire buffer to utop
by pressing
C-c C-b
via the function utop-eval-buffer
. If you use the dune
build
system and configure Emacs appropriately (instructions on how to do this are in
the utop documentation), a dialog will pop up saying: “utop command line: opam
config exec – dune utop . – -emacs”. Press RET
to evaluate the code.
You might have seen a message saying “Error: unbound module Base”. This code
uses Jane Street’s Base alternative standard library which makes things a bit
more complicated, since utop
does not know about Base by default.
To solve this, create a new file in the same directory called .ocamlinit
.
utop
reads this file before starting and executes the commands specified. You
just need to include a single line to load the Base library into utop
:
#require "base";;
Now try the previous steps again to load the sum_list
function into utop
. If
this still doesn’t work, make sure your opam
environment is set up correctly
by running the command opam switch
in a terminal and following the
instructions.
Once everything is working, go ahead and test the function in the REPL by
running sum_list [1; 2; 3];;
(the double semicolons at the end of the line are
important because utop
uses them to mark the end of an expression). If you
want to make changes to the function, simply switch back to the OCaml buffer,
edit the code, and send it back to utop
.
Working with multiple files in the REPL
The technique I described above works great within a single file, but things get
complicated once you send code from multiple files to the same utop
instance.
For example, say you made the sum_list
function within a file called
test.ml
and sent that code to utop
. Now you want to use Test.sum_list
within another file, so you create a new file called use_test.ml
which
implements a new function:
let double_sum_list list = (Test.sum_list list) * 2
Now when you go to send this new function to utop
, you run into an error:
“Error: Unbound module Test”.
Here’s the full sample utop
session:
utop[0]> open Base
let sum_list list = List.fold ~f:( + ) ~init:0 list
;;
val sum_list : int list -> int = <fun>
utop[1]> sum_list [1; 2; 3];;
- : int = 6
utop[2]> let double_sum_list list = (Test.sum_list list) * 2
;;
Error: Unbound module Test
Since OCaml isn’t really made to be an interactive programming language, there
isn’t a clean solution for this problem as far as I’m aware. However, you can
hack around this using the same .ocamlinit
file that I mentioned before.
Kill utop
and modify the .ocamlinit
file to look like this:
#require "base";;
#mod_use "test.ml";;
The #mod_use
function tells utop
to import the given file into the REPL as a
module. This is important because it lets us call sum_list
as Test.sum_list
.
#mod_use
essentially wraps up the functions from the file into a module and
sends that module to be evaluated in the REPL, which is basically how the OCaml
compiler treats OCaml files. We don’t want to change our development style to
work with the REPL since utop
is configurable enough.
There is one caveat with this approach: you have to edit .ocamlinit
and
restart utop
whenever you create a new file. If you switch files (say you were
sending code from use_test.ml
to the REPL but now want to work with
test.ml
), you have to restart utop
each time to ensure that it has the most
up-to-date version of all your files/modules. This is a bit of a pain and I’m
not sure if there’s a solution to this problem given OCaml’s static nature.
Pretty-printing
A major part of interactive development is seeing the results of functions in the REPL. Since OCaml has a strong type system without dynamic dispatch, you can only print strings—this means that you have to write functions to convert your user-defined types (which are everywhere in idiomatic OCaml code) to strings each time you want to print them. This is a pain, but luckily there’s an elegant solution: ppx.
ppx
is a syntax extension to OCaml which acts as a macro that automatically
generates code to pretty-print a custom type (ppx_deriving.show
), generate
equality functions (ppx_deriving.eq
), etc.
To pretty-print custom types annotated with [@@deriving show]
in utop
, you’ll need to
once again modify the .ocamlinit
file and add the following line:
#install_printer Module.pp;;
where Module
is the name of the module which has the corresponding pp
function. Here’s an example of one such module that pretty-prints a custom
hash-table with the Depths
module, where type t
… [@@deriving show]
refers
to the Resolver.t
type:
module Depths = struct
type t = (string, int) Hashtbl.t
let pp ppf values =
Caml.Format.open_hovbox 1;
Caml.Format.print_cut ();
if Hashtbl.length values = 0
then Caml.Format.fprintf ppf "@[<hov 2>{}@]"
else (
Caml.Format.fprintf ppf "@[<hov 1>{@ @]";
Hashtbl.iteri values ~f:(fun ~key ~data ->
Caml.Format.fprintf ppf "@[<hov 2>%s: %d,@ @]" key data);
Caml.Format.fprintf ppf "@[<hov 1>}@]");
Caml.Format.close_box ()
;;
end
type t =
{ statements : Parser.statement list
; scopes : Scopes.t
; depths : Depths.t
; parsed_statements : Parser.statement list
}
[@@deriving show]
Here are the corresponding lines in .ocamlinit
which tell utop
which types
to pretty-print (the above code is from a file called resolver.ml
):
#install_printer Resolver.pp;;
#install_printer Resolver.Depths.pp;;
Now utop
knows to call the respective pp
function whenever it needs to print
type information for the corresponding module. I needed to write the custom
Depths.pp
function by hand since ppx_deriving.show
is not powerful enough to
work for all custom types. This is one drawback of strong static type systems.
Tracing function execution
Say you want to now debug the resolve
function in your Resolver
module, but
the return value of resolve
is of type Resolver.t
. If you didn’t have the
[@@deriving show]
ppx
annotation on type t
and didn’t write the custom
Scopes.pp
and Depths.pp
functions, this would be part of the output of
tracing a call to Resolver.resolve
in utop
(I cut off the rest of the output
since it wasn’t important):
utop[1]> #trace Resolver.resolve;;
Resolver.resolve is now traced.
utop[2]> Scanner.make_scanner "var x = 1; { var y = 2; }"
|> Scanner.scan_tokens
|> Parser.make_parser
|> Parser.parse
|> Resolver.make_resolver
|> Resolver.resolve;;
Resolver.resolve <--
{Resolver.statements =
[Parser.VarDeclaration
{Parser.name =
{Scanner.token_type = Scanner.Identifier; lexeme = "x";
literal = Value.LoxNil; line = 1};
init =
Parser.Literal
{Parser.token =
{Scanner.token_type = Scanner.Number; lexeme = "1";
literal = Value.LoxNumber 1.; line = 1};
value = Value.LoxNumber 1.}};
Parser.Block
[Parser.VarDeclaration
{Parser.name =
{Scanner.token_type = Scanner.Identifier; lexeme = "y";
literal = Value.LoxNil; line = 1};
init =
Parser.Literal
{Parser.token =
{Scanner.token_type = Scanner.Number; lexeme = "2";
literal = Value.LoxNumber 2.; line = 1};
value = Value.LoxNumber 2.}}]];
scopes = <abstr>; depths = <abstr>;
Notice this last line: scopes = <abstr>; depths = <abstr>;
. The <abstr>
value indicates that OCaml does not know how to print values of the Scopes.t
or Depths.t
type since there are no dedicated pp
functions for those types.
Once I added the [@@deriving show]
annotation back to type t
, wrote the
Scopes.pp
and Depths.pp
functions, and added the relevant #install_printer
lines to .ocamlinit
, this was the full output of the same trace to
Resolver.resolve
:
utop[1]> #trace Resolver.resolve;;
Resolver.resolve is now traced.
utop[2]> Scanner.make_scanner "var x = 1; { var y = 2; }"
|> Scanner.scan_tokens
|> Parser.make_parser
|> Parser.parse
|> Resolver.make_resolver
|> Resolver.resolve;;
Resolver.resolve <--
{ Resolver.Resolver.statements =
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "x"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "1"; literal = (Value.Value.LoxNumber 1.);
line = 1 };
value = (Value.Value.LoxNumber 1.) })
});
(Parser.Parser.Block
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "y"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "2"; literal = (Value.Value.LoxNumber 2.);
line = 1 };
value = (Value.Value.LoxNumber 2.) })
})
])
];
scopes = {}; depths = {};
parsed_statements =
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "x"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "1"; literal = (Value.Value.LoxNumber 1.);
line = 1 };
value = (Value.Value.LoxNumber 1.) })
});
(Parser.Parser.Block
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "y"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "2"; literal = (Value.Value.LoxNumber 2.);
line = 1 };
value = (Value.Value.LoxNumber 2.) })
})
])
]
}
Resolver.resolve <--
{ Resolver.Resolver.statements =
[(Parser.Parser.Expression
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "1"; literal = (Value.Value.LoxNumber 1.); line = 1 };
value = (Value.Value.LoxNumber 1.) }))
];
scopes = {}; depths = {};
parsed_statements =
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "x"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "1"; literal = (Value.Value.LoxNumber 1.);
line = 1 };
value = (Value.Value.LoxNumber 1.) })
});
(Parser.Parser.Block
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "y"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "2"; literal = (Value.Value.LoxNumber 2.);
line = 1 };
value = (Value.Value.LoxNumber 2.) })
})
])
]
}
Resolver.resolve -->
{ Resolver.Resolver.statements =
[(Parser.Parser.Expression
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "1"; literal = (Value.Value.LoxNumber 1.); line = 1 };
value = (Value.Value.LoxNumber 1.) }))
];
scopes = {}; depths = {};
parsed_statements =
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "x"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "1"; literal = (Value.Value.LoxNumber 1.);
line = 1 };
value = (Value.Value.LoxNumber 1.) })
});
(Parser.Parser.Block
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "y"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "2"; literal = (Value.Value.LoxNumber 2.);
line = 1 };
value = (Value.Value.LoxNumber 2.) })
})
])
]
}
Resolver.resolve <--
{ Resolver.Resolver.statements =
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "y"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "2"; literal = (Value.Value.LoxNumber 2.);
line = 1 };
value = (Value.Value.LoxNumber 2.) })
})
];
scopes = {}; depths = {};
parsed_statements =
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "x"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "1"; literal = (Value.Value.LoxNumber 1.);
line = 1 };
value = (Value.Value.LoxNumber 1.) })
});
(Parser.Parser.Block
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "y"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "2"; literal = (Value.Value.LoxNumber 2.);
line = 1 };
value = (Value.Value.LoxNumber 2.) })
})
])
]
}
Resolver.resolve <--
{ Resolver.Resolver.statements =
[(Parser.Parser.Expression
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "2"; literal = (Value.Value.LoxNumber 2.); line = 1 };
value = (Value.Value.LoxNumber 2.) }))
];
scopes = { y: declared, }; depths = {};
parsed_statements =
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "x"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "1"; literal = (Value.Value.LoxNumber 1.);
line = 1 };
value = (Value.Value.LoxNumber 1.) })
});
(Parser.Parser.Block
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "y"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "2"; literal = (Value.Value.LoxNumber 2.);
line = 1 };
value = (Value.Value.LoxNumber 2.) })
})
])
]
}
Resolver.resolve -->
{ Resolver.Resolver.statements =
[(Parser.Parser.Expression
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "2"; literal = (Value.Value.LoxNumber 2.); line = 1 };
value = (Value.Value.LoxNumber 2.) }))
];
scopes = { y: declared, }; depths = {};
parsed_statements =
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "x"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "1"; literal = (Value.Value.LoxNumber 1.);
line = 1 };
value = (Value.Value.LoxNumber 1.) })
});
(Parser.Parser.Block
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "y"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "2"; literal = (Value.Value.LoxNumber 2.);
line = 1 };
value = (Value.Value.LoxNumber 2.) })
})
])
]
}
Resolver.resolve -->
{ Resolver.Resolver.statements =
[(Parser.Parser.Expression
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "2"; literal = (Value.Value.LoxNumber 2.); line = 1 };
value = (Value.Value.LoxNumber 2.) }))
];
scopes = { y: declared, }; depths = {};
parsed_statements =
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "x"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "1"; literal = (Value.Value.LoxNumber 1.);
line = 1 };
value = (Value.Value.LoxNumber 1.) })
});
(Parser.Parser.Block
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "y"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "2"; literal = (Value.Value.LoxNumber 2.);
line = 1 };
value = (Value.Value.LoxNumber 2.) })
})
])
]
}
Resolver.resolve <--
{ Resolver.Resolver.statements =
[(Parser.Parser.Expression
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "2"; literal = (Value.Value.LoxNumber 2.); line = 1 };
value = (Value.Value.LoxNumber 2.) }))
];
scopes = {}; depths = {};
parsed_statements =
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "x"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "1"; literal = (Value.Value.LoxNumber 1.);
line = 1 };
value = (Value.Value.LoxNumber 1.) })
});
(Parser.Parser.Block
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "y"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "2"; literal = (Value.Value.LoxNumber 2.);
line = 1 };
value = (Value.Value.LoxNumber 2.) })
})
])
]
}
Resolver.resolve -->
{ Resolver.Resolver.statements =
[(Parser.Parser.Expression
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "2"; literal = (Value.Value.LoxNumber 2.); line = 1 };
value = (Value.Value.LoxNumber 2.) }))
];
scopes = {}; depths = {};
parsed_statements =
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "x"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "1"; literal = (Value.Value.LoxNumber 1.);
line = 1 };
value = (Value.Value.LoxNumber 1.) })
});
(Parser.Parser.Block
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "y"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "2"; literal = (Value.Value.LoxNumber 2.);
line = 1 };
value = (Value.Value.LoxNumber 2.) })
})
])
]
}
Resolver.resolve -->
{ Resolver.Resolver.statements =
[(Parser.Parser.Expression
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "2"; literal = (Value.Value.LoxNumber 2.); line = 1 };
value = (Value.Value.LoxNumber 2.) }))
];
scopes = {}; depths = {};
parsed_statements =
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "x"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "1"; literal = (Value.Value.LoxNumber 1.);
line = 1 };
value = (Value.Value.LoxNumber 1.) })
});
(Parser.Parser.Block
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "y"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "2"; literal = (Value.Value.LoxNumber 2.);
line = 1 };
value = (Value.Value.LoxNumber 2.) })
})
])
]
}
- : Resolver.t =
{ Resolver.Resolver.statements =
[(Parser.Parser.Expression
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "2"; literal = (Value.Value.LoxNumber 2.); line = 1 };
value = (Value.Value.LoxNumber 2.) }))
];
scopes = {}; depths = {};
parsed_statements =
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "x"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "1"; literal = (Value.Value.LoxNumber 1.); line = 1 };
value = (Value.Value.LoxNumber 1.) })
});
(Parser.Parser.Block
[(Parser.Parser.VarDeclaration
{ Parser.Parser.name =
{ Scanner.Scanner.token_type = Scanner.Scanner.Identifier;
lexeme = "y"; literal = Value.Value.LoxNil; line = 1 };
init =
(Parser.Parser.Literal
{ Parser.Parser.token =
{ Scanner.Scanner.token_type = Scanner.Scanner.Number;
lexeme = "2"; literal = (Value.Value.LoxNumber 2.);
line = 1 };
value = (Value.Value.LoxNumber 2.) })
})
])
]
}
utop[8]>
Notice how utop
now knows how to print the Scopes.t
and Depths.t
types,
like scopes = { y: declared, }; depths = {};
, instead of just scopes = <abstr>; depths = <abstr>;
. This technique is incredibly useful for debugging
by tracing functions in the REPL and using the REPL interactively in general.
I hope this overview of interactive OCaml development with utop
was useful.
Even though OCaml is a language that has an uncompromisingly strict static type
system, it’s still possible to get some of the useful interactive features
of more dynamic languages like Lisp through a configurable plugin-based REPL and
syntax extensions that help minimize boilerplate. Sometimes you really can have
your cake and eat it too!