Hi,
The upcoming OCaml 4.08 release will allow developers to define custom bindings operators. We were eager to use this feature in the code of Dune but because we are currently keeping compatibility with all versions of OCaml since 4.02, we decided to implement a preprocessor shim for older OCaml versions. Given that this shim works quite well, we are also making it available for users of Dune starting with version 1.8 which will be released soon. This post explains how to use this new feature.
The future_syntax preprocessor
If you want to use custom bindings in your code but need to keep your code compatible with OCaml < 4.08, you can use the special future_syntax
preprocessor introduced in Dune 1.8. To do that, simply add the following field to your library/executable stanza:
(preprocess future_syntax)
When using OCaml >= 4.08, this is exactly equivalent to just deleting this field. This means that future_syntax
doesn’t add overheard when using a recent version of the compiler. When using using OCaml < 4.08, this will automatically add a pre-processor that will translate special let+
, let*
, and+
, … operators into valid pre-4.08 OCaml code, allowing your code to compile with an older compiler.
Limitation
The shim preprocessor converts bindings operators to OCaml identifiers of the form let__XXX
and and__XXX
. For instance, let+*
is translated to let__plus_star
. So you must make sure to not use such identifiers in your code.
Complete example
The following example uses the future_syntax
preprocessor and bindings operators in code using the cmdliner library.
dune
file:
(executable
(name foo)
(libraries cmdliner)
(preprocess future_syntax))
foo.ml
file:
open Cmdliner
let ( let+ ) t f =
Term.(const f $ t)
let ( and+ ) a b =
Term.(const (fun x y -> x, y) $ a $ b)
let term =
let+ a = Arg.(value & flag & info ["a"] ~doc:"blah")
and+ b = Arg.(value & flag & info ["b"] ~doc:"blah")
and+ c = Arg.(value & flag & info ["c"] ~doc:"blah")
in
Printf.printf "a=%B b=%B c=%B\n" a b c
let cmd = (term, Term.info "foo" ~version:"v1.0.3" ~doc:"example")
let () = Term.(exit @@ eval cmd)
You can test this example with:
$ dune exec ./foo.exe -- -a -b
Without bindingins operators, foo.ml
would have to be written as follow:
open Cmdliner
let term =
let main a b c =
Printf.printf "a=%B b=%B c=%B\n" a b c
in
Term.(const main
$ Arg.(value & flag & info ["a"] ~doc:"blah")
$ Arg.(value & flag & info ["b"] ~doc:"blah")
$ Arg.(value & flag & info ["c"] ~doc:"blah")
)
let cmd = (term, Term.info "foo" ~version:"v1.0.3" ~doc:"example")
let () = Term.(exit @@ eval cmd)
Which shows that binding operators are especially nice when working with such API; indeed, without binding operators the authors and readers of the code have to manually match the order of arguments passed to main
with the order of the Arg.(...)
expressions inside the Term.(...)
expression. With binding operators, the OCaml variable to which the evaluation of the command line argument is bound is right next to its definition, which is much nicer to read and write.