Well, this is exactly how I’m developing my code every day, and the style that I’m trying to promote. I’m using Emacs as my editor of choice, I believe there should be some sort of integration for other editors (I think I’ve seen something for vscode, but I hope that users of other editors will jump in and advertise their tools).
Initial Setup
Here is my workflow. I’m using Emacs’s Tuareg mode, coupled with ocp-indent for consistent indentation, and merlin for completion and incremental type checking. This is how you can set everything up. I’m not using utop, but an integration with it is also available and one may choose to use utop to be able to use Dune’s utop integration.
Basic workflow
My basic workflow follows your definition, except that if you need to use some external libraries you need to load them into the toplevel (remember that the toplevel is just a simple OCaml program, that is not by default linked with any libraries other than the standard library).
To make OCaml packages available to ocaml
/utop
toplevel we need to use the topfind
utility, which is installed as a part of the ocamlfind
package. Given that the latter package is the dependency of merely everything you will most likely have it installed. Therefore our first directive to the toplevel would be
#use "topfind";;
Make sure that you actually type #
this is how directives to toplevel are issued. Also, this command is a good candidate to add to your ~/.ocamlinit
file, which is evaluated every time you start any toplevel.
Hint: instead of typing ;;
use S-Ret
to send the phrase to the toplevel.
Now, we can load any library to the toplevel. This could be done with the require
directive, e.g.,
#require "core_kernel";;
Hint: to get the list of available packages use the list
directive, e.g.,
#list;;
Now, we can just hit C-c C-e
to send our phrases to OCaml. Or C-c C-b
to send the whole buffer, or C-c C-r
to evaluate just a selected region.
Pro tips
Using printers.
While OCaml will try its best to print everything, sometimes it is necessary to install your own printer. You can have many different printers for your data, to give you different views on what is going on, so that you can switch between one and another. (For example, I’m even having a rendering overlay in Emacs, which renders graphs and trees directly in the toplevel. I used to have the same for matrices, when I was doing heavy math).
The install_printer
directive enables custom printers for your data, which is very useful during development and debugging. It takes a function, which should have type Format.formatter -> t -> unit
where t
is the type of your data, e.g.,
type t = Student of int
let pp_name ppf (Student id) =
Format.fprintf ppf "%s" (Hashtbl.find_exn names id)
and now we can install it,
#install_printer pp_name
Tracing
OCaml toplevels also provide a nice feature called tracing, which shows how your functions are invoked and what they return. It is much easier to use then the good old debugging output. Especially, when you provide your custom printers (yes tracer will use the same printing facility, so everything will work seamlessly).
To trace a function use the trace
directive, e.g.,
#trace find_best_student;;
To stop tracing use the #untrace
Tip: to get the list of available directives, use the help
directive.
Developing Large Applications
The REPL driven development suits best the bottom-up style of development. When the application grows it becomes harder and harder to use toplevel. But with the right approach, it scales! Just keep in mind that REPL driven development actually facilitates modular design, and if you keep struggling with toplevel when your application grows it is an indicator that you have problems with your design. The idea of the bottom up development is that you develop small modules which are independent and do not need a lot of context to debug a module.
My approach for developing large applications with REPL is to debug each individual piece independently. Once they are debugged I can build and install them using normal building facitilites and then load them using the require
directive like I was loading core_kernel
and other dependencies. You can also use Dune utop integration, to run utop (from Emacs of course) that will make your libraries readily available. You can also build your custom toplevels, when necessary.
Another trick, that I find useful in real life. When for some reason your can’t load a dependency of your module in the toplevel, you can easily stub it, e.g.,
(* let's stub some complex external library which is developed
by some other guy, and is still not yet ready *)
module Database : Database.S = struct
type t = string
let connect str = printf "connect %s" str;
let select conn query =
printf "%s> %s" conn query;
[]
end
(* here comes our code that needs the database, which is not yet ready,
but it doesn't stop us anymore *)
let start_driviving env =
let db = Database.connect env.main_host in
let waypoints = Database.select db waypoints_query in
drive_through waypoints
Further reading
P.S. I will also hope that users of other editors will jump in and share their own experience.