March 8, 2020

Interactive OCaml Development

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!

© Samarth Kishor 2020

Powered by Hugo & Kiss.