Let+ syntax backported to OCaml >= 4.02

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.

17 Likes

This looks awesome! Just got a question about versioning of the future_syntax preprocessor. I like how easy it is to specify it in the dune files, but I’m concerned about what will happen if one day you want to deprecate this preprocessor and support yet another “future_syntax”.

For example, in the python2 community, it’s common to see people specifically select which future syntaxes to use by writing:

from __future__ import print_function
4 Likes

future_syntax will be versioned in the same way as other features provided by dune. If in the future we decide to change what future_syntax means, then its meaning will simply be tied to the (lang dune x.y) you write in your dune-project file.

That said, I don’t think we will be able to provide much more than custom binding operators in dune. Indeed, the only form of extensibility provided by the OCaml parser is a lexer hook and ast mappers. This was enough to easily retrofit the let+ syntax, but it won’t be enough for other more complex new syntax forms. At some point, we would need to import the whole OCaml frontend, which doesn’t seem viable.

Is future_syntax specific to code using OCaml syntax or would it work with Reason syntax as well?

1 Like

It is specific to code using OCaml syntax. For Reason, refmt would be the right place to provide such a feature.

2 Likes

Thanks @jeremiedimino, I thought that was the case but wanted to be sure.

If it’s only going to provide let syntax then perhaps a more specific name like: let_syntax_shim or binding_operators_shim would be better.

4 Likes

We’ll also support things like match+ if they are added to the language

For some reason it doesn’t work with functors for me:

module type Monad = sig
  type 'a t
  val  bind : 'a t -> ('a -> 'b t) -> 'b t
end

module M (X : Monad) = struct
  let ( let* ) = X.bind
end

let comp () =
  let open M (Option) in (* Syntax error *)
  let* x = Some 42 in
  None

File "lib/let/let.ml", line 53, characters 13-14:
Error: Syntax error

Note that let open M (Option) in ... is a new syntax in 4.08. It is not covered by (preprocess future_syntax).

Ah, indeed. It doesn’t work with dune either, right?

Do you mean when you use (preprocess future_syntax)? No, it doesn’t. We couldn’t provide a shim for this syntax without copy&pasting a lot of code from the compiler. let+ was a special case.

1 Like

How do we use future_syntax with other ppx? For example, I am trying to use future_syntax with ppx_let but getting an error from dune.

$ cat dune
(executable
 (name test_let_syntax)
 (preprocess
  (future_syntax)
  (pps lwt_ppx ppx_let))
 (libraries lwt_ppx uri lwt lwt.unix str))

$ cat test.ml
let (let*) = Lwt.bind

let () = 
  let* line = Lwt_io.(read_line stdin) in 
  Lwt_io.printlf("%s", line)

The above is giving me the compilation error as below,

File "src/dune", line 7, characters 2-23:
7 |   (pps lwt_ppx ppx_let))
      ^^^^^^^^^^^^^^^^^^^^^
Error: Too many argument for preprocess

What gives?

1 Like

I had the same question. It doesn’t work with other preprocessors. Here’s the response. tl;dr: It would need to be implemented in ppxlib in order to become compatible with other preprocessors, but doing so requires non-trivial amount of work.

What is holding you back from upgrading to the latest OCaml compiler?

EDIT: Apparently there’s now better support in Dune 1.10. I haven’t tested yet though. Please see diml’s post below.

Actually, following a few recent changes with dune 1.10 you should be able to do: (pps lwt_ppx ppx_let -- -pp %{bin:ocaml-syntax-shims}).

2 Likes

Trying that gets me the following:
Error: %{bin:..} isn't allowed in this position

This is on dune 1.10.0

Should I raise an issue in GitHub?

Ah, we must have missed something here. Yes, please raise an issue on GitHub :slight_smile:

Check here for a solution: https://github.com/ocaml/dune/issues/2262

1 Like