tweag / topiary

https://topiary.tweag.io/
MIT License
579 stars 29 forks source link

OCaml: too much indentation and newlines around final-argument continuations #718

Closed jonsterling closed 2 months ago

jonsterling commented 3 months ago

Hi! Thanks very much for developing topiary; this is the most promising formatter for OCaml that I have seen so far, and it looks really powerful. I'm eager to start using it, but there are some aspects of the OCaml format style that are preventing me.

Is your feature request related to a problem? Please describe.

It is very common in OCaml code for the final argument to a function be some kind of continuation. One often writes something like:

items |> List.iter @@ fun item ->
(* something involving 'item' *)

This is formatted by topiary as follows:

items |> List.iter @@
  fun item ->
    (* something involving 'item' *)

Almost every newline and indent in this example is problematic: the newline before fun, the indentation before fun and the indentation before the body. The example above may not look so bad, but consider the following real code from Forester, as formatted by Topiary:

let eval_tree ~addr ~source_path (tree : Syn.tree) =
  let fm = { T.empty_frontmatter with addr; source_path } in
  Frontmatter.run ~init: fm @@
    fun () ->
      Emitted_trees.run ~init: [] @@
        fun () ->
          Jobs.run ~init: [] @@
            fun () ->
              Heap.run ~init: Env.empty @@
                fun () ->
                  Lex_env.run ~env: Env.empty @@
                    fun () ->
                      Dyn_env.run ~env: Env.empty @@
                        fun () ->
                          let main = eval_tree_inner ~addr tree in
                          let side = Emitted_trees.get () in
                          let jobs = Jobs.get () in
                          { main; side; jobs }

The above is Topiary's formatting of the following original code, which I feel is formatted in a way that scales a lot better to this kind of code:

let eval_tree ~addr ~source_path (tree : Syn.tree) =
  let fm = {T.empty_frontmatter with addr; source_path} in
  Frontmatter.run ~init:fm @@ fun () ->
  Emitted_trees.run ~init:[] @@ fun () ->
  Jobs.run ~init:[] @@ fun () ->
  Heap.run ~init:Env.empty @@ fun () ->
  Lex_env.run ~env:Env.empty @@ fun () ->
  Dyn_env.run ~env:Env.empty @@ fun () ->
  let main = eval_tree_inner ~addr tree in
  let side = Emitted_trees.get () in
  let jobs = Jobs.get () in
  {main; side; jobs}

Describe the solution you'd like

I would like my code style depicted above to be preserved, or at least roughly preserved (I am open to change my style if it still meets my goals of not causing things to march endlessly to the right of my screen). Here are a few specific points:

  1. I do not think that fun args -> <<newline>> body should have an indent inserted into the body, e.g. fun args -> <<newline>> <<indent>> body.
  2. I do not think that a final argument continuation should have a newline inserted before it.

I am not totally certain if the above two principles are adequate to generate the behaviour I would like, or are consistent with the rest of the formatting style of Topiary, but I think this would be a good step. I do think that my proposal is more consistent with the existing formatting of let ... in .... I'm also open to other ideas that would achieve the goals.

Describe alternatives you've considered The alternative I have considered is to continue using ocp-indent, which has a reasonably good formatting style that preserves my intentions, but is not quite as normative in its enforcement of style as I would like.


Thanks again! This is great work and I'm really eager to be able to start using it for my projects.