Mini OCaml Tutorial
Task: Simple Formula Library
We think it is best to illustrate the problems encountered and how we decided to organize code in Toss on an example task. In this tutorial, we will build a very simple program to manipulate propositional formulas. Let's state the task as follows: Build a program to compute the NNF of formulas given on input. We will construct the program in several steps, starting with basic OCaml and adding complexity.Step 1 — The Functions
In this step we construct the main file with types and functions that do the work. This requires nothing more than the basic OCaml tutorial and results in the following file.Formula.ml
type formula = | Var of string | Not of formula | Or of formula * formula | And of formula * formula let rec str = function | Var s -> s | Not f -> "(not " ^ (str f) ^ ")" | Or (f, g) -> "(" ^ (str f) ^ " or " ^ (str g) ^ ")" | And (f, g) -> "(" ^ (str f) ^ " and " ^ (str g) ^ ")" let rec nnf ?(negate=false) = function | Var s -> if not negate then Var s else Not (Var s) | Not f -> nnf ~negate:(not negate) f | Or (f, g) -> if not negate then Or (nnf f, nnf g) else And (nnf ~negate:true f, nnf ~negate:true g) | And (f, g) -> if not negate then And (nnf f, nnf g) else Or (nnf ~negate:true f, nnf ~negate:true g) let _ = print_endline (str (nnf (Not (And (Var "X", Not (Var "Y"))))))Note that we have only two functions: str to print a formula to string and nnf to convert it to NNF, with an optional argument that allows to negate the formula we are converting. The last line calls the functions on an example formula (not (X and not Y)), and if you execute ocaml Formula.ml you will get the result, namely ((not X) or Y).
Step 2 — Splitting and Compiling
During development, we will almost never keep implementations of types and functions together with the calls which are user-visible — like the one in the last line in the code above. To account for this, let us split the above code into a Formula.ml file containing everything but the last line, and a Main.ml file with two lines: one saying open Formula and another with exactly the last line of the code above. You can now compile these files by running ocamlopt Formula.ml Main.ml and execute the resulting file ./a.out. Since with more files it becomes harder and harder to account for the dependencies, we will not call ocamlopt but use ocamlbuild instead. It is a build system which automatically recognizes dependencies between OCaml files, so to build our two files you just need to execute ocamlbuild Main.native. This results in a file Main.native which, when executed, will print out the same result ((not X) or Y). Note that ocamlbuild will create a directory _build to store intermediate files.Step 3 — Lexer and Parser
To allow the user to input formulas, we have to construct a lexer and a parser. We will not do it by hand, but use a parser generator called menhir. Here is the simple lexer file we use for lexing formulas.Lexer.mll
{ type token = | ID of (string) | AND | OR | NOT | OP | CL | EOF } rule lex = parse | [' ' '\t'] { lex lexbuf } | "and" { AND } | "or" { OR } | "not" { NOT } | "(" { OP } | ")" { CL } | ['A'-'Z' 'a'-'z' '_']['0'-'9' 'A'-'Z' 'a'-'z' '_']* as s { ID (s) } | eof { EOF }and here is a very simple parser.
Parser.mly
%token <string> ID %token AND OR NOT OP CL EOF %left OR AND %nonassoc NOT %{ open Lexer open Formula %} %start parse_formula %type <Formula.formula> parse_formula formula_expr %% %public formula_expr: | ID { Var ($1) } | NOT formula_expr { Not ($2) } | formula_expr AND formula_expr { And ($1, $3) } | f = formula_expr OR g = formula_expr { Or (f, g) } | OP f = formula_expr CL { f } parse_formula: | formula_expr EOF { $1 }Note that the syntax is similar to the Yacc system, and a complete documentation can be found in the reference manual of menhir. The Main.ml file can now look like this.
Main.ml
open Formula let formula_of_string s = Parser.parse_formula Lexer.lex (Lexing.from_string s) let _ = print_endline (str (nnf (formula_of_string "not (X and not Y)")))and to build it we need to execute to tell ocamlbuild how to call menhir, as follows.
ocamlbuild -use-menhir -menhir "menhir --external-tokens Lexer" Main.nativeThe result of executing ./Main.native should not change, so it should still be ((not X) or Y).
Step 4 — Testing with oUnit
Having a lexer and a parser, we can now complete the simple program for reading formulas from the input and calculating their NNF. But we would also like to keep the test we had above — to make sure that, during further development, the NNF of at least this sample formula will remain correct. This is called unit testing and we will use the oUnit package for it. A small test file with our one test is given below.FormulaTest.ml
open OUnit open Formula let formula_of_string s = Parser.parse_formula Lexer.lex (Lexing.from_string s) let tests = "Formula" >::: [ "nnf" >:: (fun () -> assert_equal ~printer:(fun x -> str x) (formula_of_string "((not X) or Y)") (nnf (formula_of_string ("not (X and not Y)"))) ); ]We can now complete the Main.ml file. We will make it run the tests if the option -t is given or read a formula from the input and convert it to NNF otherwise.
Main.ml
open Formula let formula_of_string s = Parser.parse_formula Lexer.lex (Lexing.from_string s) let main = let run_tests = ref false in let opts = [ ("-t", Arg.Unit (fun () -> run_tests := true), "run unit tests"); ] in Arg.parse opts (fun _ -> ()) "Try -help for help or one of the following."; if !run_tests then ignore (OUnit.run_test_tt ~verbose:true FormulaTest.tests) else print_endline (str (nnf (formula_of_string (read_line ()))))To compile Main.ml now, with oUnit, we have to add the path to oUnit to ocamlbuild. The following command works both on Linux and on MacOSX with MacPorts. If you use OPAM, the path might be different, e.g. ~/.opam/4.00.1/lib/oUnit — check it out.
ocamlbuild -libs unix,oUnit -lflags -I,+oUnit,-I,+site-lib/oUnit -cflags -I,+oUnit,-I,+site-lib/oUnit -use-menhir -menhir "menhir --external-tokens Lexer" Main.native
Step 5 — Interface Files and Documentation
While we are ready with our small program, it is still useful to look which functions from Formula.ml we use in Main.ml. Since we just call them, we could in principle use all of them. This is not a good practice, especially for larger modules with many functions which should remain private in the module. To declare which functions should be public, and to document them, we use mli files with documenting comments with double **. Here is an example of the mli file for our Formula module.Formula.mli
(** Module for computing the NNF of formulas. *) (** Representation of propositional formulas. *) type formula = | Var of string (** Propositional variable *) | Not of formula (** Negation of a formula *) | Or of formula * formula (** Disjunction *) | And of formula * formula (** Conjunction *) (** Convert a formula to a string. *) val str: formula -> string (** Negation normal form of a formula. *) val nnf: ?negate: bool -> formula -> formulaTo build documentation in HTML from this file, create a new file called Formula.odocl listing the mli files to document, i.e. in this case containing just a single line saying Formula.
Formula.odocl
FormulaWith this file present, run the following command and look at the file index.html in the created directory Formula.docdir.
ocamlbuild -libs unix,oUnit -lflags -I,+oUnit,-I,+site-lib/oUnit -cflags -I,+oUnit,-I,+site-lib/oUnit -use-menhir -menhir "menhir --external-tokens Lexer" Formula.docdir/index.html
Step 6 — JavaScript Interface
While the program for computing the NNF is finished now and can be run from the command line, most people nowadays do not use the command line but rather run their programs in the browser. OCaml programs can be compiled to JavaScript and run in the browser using js_of_ocaml. While you can find the complete manual on the above site, we present here a simple way of using js_of_ocaml to compute NNF in the browser. Below is the HTML file with the interface and the JavaScript calls, have a look at it and note the new Worker ("JsClient.js"); call, which we explain in detail below.index.html
<html> <head> <meta http-equiv="Content-Type" content="text/xhtml+xml; charset=UTF-8" /> <title>Tutorial</title> <script type="text/javascript"> <!-- var worker = new Worker ("JsClient.js"); var worker_handler = new Object (); worker.onmessage = function (m) { if (typeof m.data == 'string') { console.log("" + m.data); } else { console.log ("[ASYNCH] back from " + m.data.fname); var handler = worker_handler[m.data.fname]; handler (m.data.result); } } function ASYNCH (action_name, action_args, cont) { worker_handler[action_name] = cont; worker.postMessage ({fname: action_name, args: action_args}); console.log ("[ASYNCH] " + action_name + " (" + action_args + ")"); } function convert_nnf () { var txt = document.getElementById ("formula").value; ASYNCH ("nnf", [txt], function (resp) { alert (resp) }) } //--> </script> </head> <body> <textarea id="formula" rows="2" cols="40"> not (X and not Y)</textarea> <button onclick="convert_nnf()">Convert to NNF</button> </body> </html>As you can see, the code above sends the content of the textarea to the function labeled nnf in the worker. Recall that web workers were introduced in HTML5 to start a parallel thread from JavaScript. We always execute compiled OCaml code in a worker thread, to make sure that it does not make your browser hang, even if the computation takes very long. The postMessage function is the standard way to pass data to the worker, and worker.onmessage is used to get the response. We wrap the calls to the worker in an ASYNCH function, which also allows us to log all calls (useful for debugging). Let us now present the worker itself, which results from compiling the following ml file.
JsClient.ml
open Formula (* Boilerplate code for calling OCaml in the worker thread. *) let js_object = Js.Unsafe.variable "Object" let js_handler = jsnew js_object () let postMessage = Js.Unsafe.variable "postMessage" let log s = ignore (Js.Unsafe.call postMessage (Js.Unsafe.variable "self") [|Js.Unsafe.inject (Js.string s)|]) let onmessage event = let fname = event##data##fname in let args = event##data##args in let handle = Js.Unsafe.get js_handler fname in let result = Js.Unsafe.fun_call handle (Js.to_array args) in let response = jsnew js_object () in Js.Unsafe.set response (Js.string "fname") fname; Js.Unsafe.set response (Js.string "result") result; Js.Unsafe.call postMessage (Js.Unsafe.variable "self") [|Js.Unsafe.inject response|] let _ = Js.Unsafe.set (Js.Unsafe.variable "self") (Js.string "onmessage") onmessage (* The NNF conversion and registration in JS. *) let formula_of_string s = Parser.parse_formula Lexer.lex (Lexing.from_string s) let js_nnf s = log ("computing nnf of " ^ (Js.to_string s)); Js.string (str (nnf (formula_of_string (Js.to_string s)))) let _ = Js.Unsafe.set js_handler (Js.string "nnf") (Js.wrap_callback js_nnf)The first, longer part of the above file is just boilerplate code which creates the postMessage function, required for a web worker, in js_of_ocaml. We will not explain it in detail here, note only the log function, which allows to log messages to the browser console. The actual NNF computation and registration in JavaScript is done in the last 6 lines of the above file. As you can see, we use the same OCaml functions formula_of_string and nnf as in previous steps. The only difference is that we have to convert from JavaScript strings to OCaml with Js.to_string and back with Js.string. In the last line, we register the js_nnf OCaml function under the name nnf in JavaScript. It is then called in JavaScript using the invocation ASYNCH ("nnf", [args], continuation), as shown above in the index.html file. Here is how we compile JsClient.ml.
ocamlbuild -use-menhir -menhir "menhir --external-tokens Lexer" \ -pp "camlp4o -I /opt/local/lib/ocaml/site-lib js_of_ocaml/pa_js.cmo" \ -cflags -I,+js_of_ocaml,-I,+site-lib/js_of_ocaml -libs js_of_ocaml \ -lflags -I,+js_of_ocaml,-I,+site-lib/js_of_ocaml JsClient.byte js_of_ocaml JsClient.byteNote that the first command is just a standard ocamlbuild call, only this time we compile to bytecode, not to a native executable, and we use the preprocessor that comes with js_of_ocaml (used e.g. in the call event##data##args). The second command invokes js_of_ocaml and compiles the bytecode to a JavaScript file JsClient.js. You can now open index.html in the browser and compute the NNF of formulas. You should also open the JavaScript console (Control-Shift-k in Firefox and Control-Shift-j in Chrome) and see the logs, to make sure it works (use google-chrome --allow-file-access-from-files to start Chrome and allow web workers to be executed from files on disk). While this tutorial presented only a very simple program, the general rules discussed above and the JavaScript boilerplate are at the core of code organization and user interface in Toss.