Let* syntax, semicolon and chaining units

I both agree and disagree with you on the flexibility on PPXes. I could see two ways to approach your problem.

One of the benefits I find to PPXes is the fact that you have a tiny string regularly reminding you in which monad (or at least which PPX extension, they might not implement monads) you live. This is more annoying when writing, and this requires defining a new PPX for each new monad that you are going to use. This is heavy and (I agree with you here) much less flexible. I do think it makes reading easier because it requires less context to read a function (“what’s the bind in this context already?”) The first solution I see would then be to define a PPX rewriter for the Lwt_result monad, say %rlwt, and then use it in your function:

let seed2 conn : (unit, 'error) result Lwt.t =
  let%rlwt () = add_author conn "John" "Doe" in
  let%rlwt () = add_author conn "Jane" "Doe" in
  let%rlwt () = add_author conn "Robert" "Doe" in
  Lwt.return_ok ()

Actually, if you have this PPX, you could also go for:

let seed2 conn : (unit, 'error) result Lwt.t =
  add_author conn "John" "Doe";%rlwt
  add_author conn "Jane" "Doe";%rlwt
  add_author conn "Robert" "Doe"

This is actually something I do in my projects, because I very often rely on the Lwt_result monad.

Now sometimes we do want to use a monad only locally and it wouldn’t make much sense to define a PPX for that. Something I use in those cases is a PPX that rewrites %m (or %monad; maybe that should actually be %bind) into an unqualified bind (PPXes only perform syntactic replacements) which then will be whatever you use locally. We lose the naming that I enjoy, but we gain some flexibility. So I would do:

let seed2 conn : (unit, 'error) result Lwt.t =
  let bind = Lwt_result.bind in
  let%m () = add_author conn "John" "Doe" in
  let%m () = add_author conn "Jane" "Doe" in
  let%m () = add_author conn "Robert" "Doe" in
  Lwt.return_ok ()

At this point, this is just the same as the let* syntax, except maybe a bit more verbose. However, you don’t get only the let, of course:

let seed2 conn : (unit, 'error) result Lwt.t =
  let bind = Lwt_result.bind in
  add_author conn "John" "Doe";%m
  add_author conn "Jane" "Doe";%m
  add_author conn "Robert" "Doe"

I am not sure I’d recommend one to do those things. I feel it is rather uncommon in the OCaml ecosystem to do that? I like it though, so I stick with PPXes. Because I spend my time redefining PPXes for all kind of things, I’ve tried to make it easy for myself by implementing ppx_monad. In particular, it helps me easily define custom monadic syntaxes, and it provides the %monad extension. I think I must be the sole user of this, though, so external feedback is most welcome.

I find your library pretty awesome! :exploding_head: :smile:

In short, for those skimming, I was able to define a dune library with the (kind ppx_rewriter) stanza, then define a new ppx with this single line:

let () = Ppx_monad.register "lwt_res" ~monad:"Lwt_result"

Then I’m able to call the following code:

let seed8 conn : (unit, 'error) result Lwt.t =
  let%lwt_res () = add_author conn "John" "Doe" in
  let%lwt_res () = add_author conn "Jane" "Doe" in
  let%lwt_res () = add_author conn "Robert" "Doe" in
  Lwt.return_ok ()
;;

let seed9 conn : (unit, 'error) result Lwt.t =
  Lwt.return_ok ()
  ; %lwt_res add_author conn "John" "Doe"
  ; %lwt_res add_author conn "Jane" "Doe"
  ; %lwt_res add_author conn "Robert" "Doe"
[@@ocamlformat "disable"]

Pretty damn cool! I’ll definitely play more with your library, the readability gains and reduction in the boilerplate (defining let*) are very nice.

The fact that you can also define and use bind on the fly is very nice too.

3 Likes