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

Add Buf_write.printf (symbolic formatter) #652

Closed
wants to merge 1 commit into from

Conversation

SGrondin
Copy link
Collaborator

@SGrondin SGrondin commented Dec 3, 2023

This PR adds Eio.Buf_write.printf and Eio.Buf_write.get_formatter.

I decided to work on this when I saw this exchange:

@mbarbin in #642

I felt like missing a function to refactor code that would make use of format string, such as Printf.printf or Format.printf equivalent. So far this looks like Eio.Flow.copy_string (Printf.sprintf "..." ...) sink. It would be great to have a more direct alternate, that probably would take the sink as first argument and format second.

@talex5

I think Buf_write.printf would be a great addition (PRs welcome).

The Format module is surprisingly powerful and I wanted to support its full breadth of features. This is not possible by dropping down to a string with Format.sprintf.

There are 2 ways to do this:

  • Custom formatter: Format.make_formatter do_write do_flush
    • do_write is (string -> int -> int -> unit)
    • do_flush is (unit -> unit)
  • Symbolic formatter: Format.make_symbolic_output_buffer, Format.formatter_of_symbolic_output_buffer and Format.flush_symbolic_output_buffer

On the surface, using a Custom Formatter appears to be the way to go. It exposes the formatter's internal buffer (along with pos and len) in the do_write callback, letting us copy it into the Buf_write buffer. The problem is that the formatter has its own buffering and we're forced to track whether printf has been called in order to issue manual flushes of the formatter every time the user calls another writing function (like Buf_write.string, Buf_write.cstruct, etc). It adds a lot of messy state to Buf_write.t.

With a symbolic formatter, the lifecycle of a call to Buf_write.printf does not leak into a dozen of other functions and it doesn't add mutable state to Buf_write.t. I'm also able to "look ahead" in the pattern and issue exactly one call to ensure_space, one call to writable_exn, and the minimal number of calls to wake_writer. The full range of Format features is supported, including %a and explicit flushes (%!, @.). The downside of the symbolic formatter is the increased allocations: it's a list of variants with nice sliced strings, not just offsets in a buffer. Overall I believe this is acceptable because of the other optimizations it enables.

@talex5
Copy link
Collaborator

talex5 commented Dec 4, 2023

This does seem surprisingly complicated! Are there benchmarks showing this is faster than using a custom formatter?

The problem is that the formatter has its own buffering and we're forced to track whether printf has been called in order to issue manual flushes of the formatter every time the user calls another writing function

So you're concerned about cases like this?

let pp = Buf_write.get_formatter w in
Format.pp_print_string pp "a";
Buf_write.string w "b"

I think it's fine if the b gets output first in this case. It's the same thing that happens with regular channels and formatters:

utop # Format.pp_print_string Format.std_formatter "a"; print_string "b";;
ba- : unit = ()

As long as Buf_write.printf flushes at the end, that will handle the common case.

@SGrondin SGrondin changed the title Add Buf_write.printf Add Buf_write.printf (symbolic formatter) Dec 4, 2023
@SGrondin
Copy link
Collaborator Author

SGrondin commented Dec 8, 2023

Closed in favor of #655

@SGrondin SGrondin closed this Dec 8, 2023
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

Successfully merging this pull request may close these issues.

2 participants