This actually comes from another case, but is simplified here. Suppose we want to implement the template method design pattern and provide a library with the implementation of the main algorithm and allow users of the library to customize it by implementing the trait. An example of this pattern can be found here.
We would then create a trait like this:
trait Command {
fn open(&mut self, file: &str);
fn read(&mut self, buf: &mut [u8]) -> bool;
fn close(&mut self);
}
This will create an interface that the library user can implement and the main algorithm, also provided in the library, would then look like this:
fn execute<C: Command>(mut command: C, path: &str) -> String {
command.open(path);
let mut sink = Sink::new();
let mut buf = [0u8; 512];
while command.read(&mut buf) {
sink.send(&buf);
}
command.close();
}
Note that in my particular case, this is the the library that I am using and it is simplified to focus on the problem. I have deliberately used the limited sized buffer here, even though it is possible to pass back data in larger chunks and I am sending out the data to some imaginary sink just to illustrate that we have an algorithm that forwards data without storing it. Also note that in contrast to Template Method in Rust this uses &mut self
rather than &self
.
We can now implement this template method for reading from a file in this manner:
struct ReadFromFile {
stream: Option<Box<dyn Read>>,
}
impl ReadFromFile {
fn new() -> ReadFromFile {
ReadFromFile { stream: None }
}
}
impl Command for ReadFromFile {
fn open(&mut self, path: &str) {
self.stream = Some(Box::new(File::open(path).unwrap()));
}
fn close(&mut self) {
self.stream = None;
}
fn read(&mut self, buf: &mut [u8]) -> bool {
match &mut self.stream {
Some(stream) => stream.read(buf).unwrap() > 0,
None => false,
}
}
}
The main function for driving this could look like this:
fn main() {
let read_all = ReadFromFile::new();
let args: Vec<String> = std::env::args().collect();
let result = execute(read_all, &args[1]);
println!("result: {:?}", result);
}
This works fine and all goes well. Now, suppose that we want to read the data from PostgreSQL instead using the postgres
crate. Following the pattern from the file example, the implementation would then look like this:
struct ReadFromDatabase {
client: Client,
stream: Option<Box<dyn Read>>,
}
impl ReadFromDatabase {
fn new() -> Result<ReadFromDatabase, postgres::Error> {
let client = Client::connect("host=localhost user=mats", NoTls)?;
Ok(ReadFromDatabase {
client,
stream: None,
})
}
}
impl Command for ReadFromDatabase {
fn open(&mut self, path: &str) {
let stmt = format!("COPY {} TO stdout", path);
self.stream = Some(Box::new(self.client.copy_out(&stmt).unwrap()));
}
fn close(&mut self) {
self.stream = None;
}
fn read(&mut self, buf: &mut [u8]) -> bool {
match &mut self.stream {
Some(stream) => stream.read(buf).unwrap() > 0,
None => false,
}
}
}
However, when compiling this, we get the following error:
error: lifetime may not live long enough
--> examples/template-method.rs:74:28
|
72 | fn open(&mut self, path: &str) {
| - let's call the lifetime of this reference `'1`
73 | let stmt = format!("COPY {} TO stdout", path);
74 | self.stream = Some(Box::new(self.client.copy_out(&stmt).unwrap()));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cast requires that `'1` must outlive `'static`
Looking for other cases, revealed this case, which is pretty close to the problem above, where you need to set a callback function.
However, the problem is focused on using a closure and suggested solution have two alternatives: add lifetimes to ReadFromDatabase
(but it does not elaborate on how), or move the object into the closure.
I think the problem here is that the stream
can outlive the client
and for that reason the code protects against an imaginary execute
method that calls read
after calling close
.
- Is it possible to write a
ReadFromDatabase
implementation by adding lifetimes and how would that look? I've made a few attempts, but as JMAA states, it "quickly becomes messy", so I am not sure it is possible to do in a reasonable way. - Is there a better pattern for implementing
ReadFromDatabase
without having to changeCommand
?