I've started trying to translate one of my more popular Python modules into Rust, and I've hit a problem with multiple mutable borrows. The high-level goal is to allow reading from one file and writing to another file at the same time while enabling cleanup of both files together. Initially, I wanted the Rust version to be used like this:
let mut inp: InPlaceFile = InPlace::new("file.txt").open()?;
let reader: &mut InPlaceReader = inp.reader();
let writer: &mut InPlaceWriter = inp.writer();
for line in reader {
writeln!(writer, "{}", some_modification(line?))?;
}
inp.save()?;
but that's invalid due to multiple mutable borrows on inp
coexisting, so obviously I have to rethink the API.
My next idea was to have InPlaceFile
implement BufRead
and Write
itself, with the implementations delegating to the appropriate inner files, but that would preclude doing desirable things like the above where BufRead::lines()
(or another consuming method) is called on the reader side while also writing out to the writer side.
Idea number three was to have InPlace::open()
return a pair of a reader and writer, but then the call to save()
would require passing both objects back (as it needs to close both filehandles), which seems like bad ergonomics and also just asking for trouble. I can't simply rely on Drop
in place of save()
, as I also need to support a discard()
call as an alternative to save()
, so at least one of those needs to be explicit.
Idea number four (actually idea number zero, before I looked into how Rust database libraries handle transactions for ideas) is for InPlace::open()
to take a FnOnce(InPlaceReader, InPlaceWriter) -> Result<T, E>
closure to which the reader & writer (or mutable references to them?) are passed, and then when the closure returns, InPlace
does either save()
or discard()
depending on whether the Result
was Ok
or not. This would likely mean that open()
's return value would have to be Result<T, InPlaceError<T, E>>
, where InPlaceError
wraps all the ways that opening, saving, and discarding could go wrong. I'm not sure how ergonomic or idiomatic this idea is.
Are there any other strategies for implementing this that I've overlooked? What would be the best approach?