Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Missing an empty surround operator? #5

Open
gasche opened this issue May 7, 2021 · 3 comments
Open

Missing an empty surround operator? #5

gasche opened this issue May 7, 2021 · 3 comments

Comments

@gasche
Copy link

gasche commented May 7, 2021

@olivier-martinot and myself are writing a pretty-printer for MidML. I got stuck on how to properly pretty-print let ... in, and I think that my final iteration suggests that a new combinator could be useful, but I don't know how to name it.

Specific question

prefix n b empty doc has a short form, jump n b.

What is the short form of surround n b empty doc empty?

Longer story

We want to pretty-print let <lhs> = <rhs> in <body>. All of the subexpressions could be long. The printing discipline we want is as follows:

  • if <lhs> or <rhs> are non-flat, they should be 2-indented on their own lines. (body is not indented)
  • otherwise pack things when they fit

For example we want to allow all of the following:

let <lhs> = <rhs> in <body>

let
  <lhs>
= <rhs> in <body>

let <lhs> =
  <rhs>
in <body>

let <lhs> = <rhs> in
<body>

(Okay, the second example looks very ugly, but (1) this regular structure makes my story applicable to other constructs that contain a mix of keywords and symbols beyond the simple prefix/surround scenarios, and (2) it never occurs in practice, and actually the fact that we never see it probably explains why we find it ugly. I don't want to make the rules or code more complex to forbid it.)

After trying many things, I ended up with the following approach where the code looks simple enough

let print_let_in lhs rhs body =
  string "let"
  ^^ surround 2 1 empty lhs empty
  ^^ string "="
  ^^ surround 2 1 empty rhs empty
  ^^ string "in"
  ^^ prefix 0 1 empty body

The prefix 0 1 empty body form can be simplified into jump 0 1 body.

For lhs and rhs, I would need a version of jump that adds a break on both sides of the main document, not just on the left. Could we have this? What would be a good name for it?

(I don't understand what jump is trying to evoke as a name, how it was chosen.)

Maybe an option would be to add a new parameter to jump for a break on the right: jump n b doc would become jump n b_left doc b_right. The old jump n b doc can be expressed with jump n b doc 0, and my use-case becomes jump 2 1 doc 1.

@fpottier
Copy link
Owner

fpottier commented May 7, 2021

I do agree that the current "high-level API" (beyond the primitive combinators) is rather messy. We never got around to cleaning it up. You are welcome to propose new combinators or new names for existing combinators (in addition to the existing names).

@gasche
Copy link
Author

gasche commented May 7, 2021

(Actually I think my idea of jump n b doc 0 does not work, as break 0 and empty are not equivalent.)

One option that is not very exciting but works is

val prefix0 : int -> int -> doc -> doc
val surround0 : int -> int -> doc -> doc

as empty-specialized versions of prefix and surround. I don't like it however, because it requires thinking of the un-natural encoding of breaks using prefix/surround to understand those names.

The core underlying abstraction here is group (nest (.... doc ...)), when the dots are optional breaks. A good name for the composition of group and nest would also do, then people would write the breaks explicitly (and consistently).

Another way to think of it

The operators that I have in mind could be documented as follows:

(* [foo1 doc] gets printed as
   ... doc ...
if doc and the group around it fit in one line, and
   ...
     doc
   ...
otherwise *) 

(* [foo2 doc] gets printed as
   ... doc...
if doc and the group around it fit in one line, and
   ...
     doc...
otherwise *) 

(I want foo1 for lhs and rhs, and foo2 for body)

This suggests possibly the names nest_inside : int -> int -> doc -> doc for the first operator, and nest_right : int -> int -> doc -> doc for the second one.

Another thing we can notice is that nest_right is only meant to be used at the right end of the group. Maybe there is a solution that takes a document list as input.

@gasche
Copy link
Author

gasche commented May 7, 2021

Current version:

let print_let_in lhs rhs body =
  group @@
  string "let"
  ^^ surround 2 1 empty lhs empty
  ^^ string "="
  ^^ surround 2 1 empty rhs empty
  ^^ string "in"
  ^^ prefix 0 1 empty body
  ^^ string "end"

Without surround and prefix:

let print_let_in lhs rhs body =
  string "let"
  ^^ group (nest 2 (break 1 ^^ lhs) ^^ break 1)
  ^^ string "="
  ^^ group (nest 2 (break 1 ^^ rhs) ^^ break 1)
  ^^ string "in"
  ^^ group (nest 0 (break 1 ^^ body))

(I don't know how to easily explain why, for lhs and rhs, there is a break inside the nest and one outside.)

With the imaginary nest_inside and nest_closing:

let print_let_in lhs rhs body =
  group @@
  string "let"
  ^^ nest_inside 2 1 lhs
  ^^ string "="
  ^^ nest_inside 2 1 rhs
  ^^ string "in"
  ^^ nest_closing 0 1 body

It looks nice, but I am not fond of reusing nest_, in particular because I already find it slightly confusing that nest does not create a group, and having nest_* derivatives that do create a group would add to fhe confusion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants