|
A challenge I've had with Rust lately is factoring initialization code into separate functions. Because of stack-based allocation it has to stay in the main function. For example: pub fn do_many(iter: &mut Iterator<Item=String>) {
let mut job_id = None;
let job_id_env = env::var("MYAPP_JOB_ID");
let mut log = if let Ok(val) = job_id_env {
write_pid_file(&val);
job_id = Some(val.clone());
let home = env::var("HOME").expect("HOME must be set");
let path = format!("{}/log/myapp-{}.log", home, val);
let path = Path::new(&path);
match File::create(&path) {
Ok(mut f) => Box::new(f) as Box<Write>,
Err(e) => {
if format!("{}", e) == "No such file or directory (os error 2)" {
Box::new(io::stdout()) as Box<Write> // oh well
} else {
panic!("Can't open log file: {}", e);
}
},
}
} else {
Box::new(io::stdout()) as Box<Write>
};
// Commit the tx if we get these signals:
let signal = chan_signal::notify(&[Signal::INT, Signal::TERM]);
let negotiator = OpenSsl::new().unwrap();
let url = env::var("MYAPP_DATABASE").unwrap_or("postgres://myapp_test:secret@localhost:5432/myapp_test".to_owned());
let tls = if url.contains("@localhost") { TlsMode::None }
else { TlsMode::Require(&negotiator) };
let conn = Connection::connect(url, tls).expect("Can't connect to Postgres");
let db = make_db_connection(&conn); // defines a bunch of prepared statements
// now we can do stuff . . .
}
I would really like to have just this: let log = open_log();
let db = prepare_db();
But those don't work, because all the temporary values are going to fall off the stack when the helper functions return. I wish rust were smart enough to make the functions put the values directly in the caller's stack frame. Alternately, I wish rust would let me say that all those temporary values should live as long as the returned thing (log and db), so it can keep them around even if I don't have variables for them.I thought maybe macros would help here, since there is no new stack frame, but they still introduce a new scope that limits the lifetime of the temporary variables. Even worse, if I want to write tests for functions that use the log and db, I need to repeat all that code again and again. I think the answer is to use Box here? I haven't worked that out yet, but it definitely feels harder than it should. And even if I can make it work, I'm a little sad that I have to give up stack-based allocation. I've also read that the answer might be OwningRef (https://crates.io/crates/owning_ref), but I'm not sure yet. I wish the Rust book had a section about it. It seems like Cow and Rc might also help me---I don't think so, but I'm not positive yet. Covering these allocation-related crates in a systematic way would be nice. Anyway, I'm just a Rust newbie, but it sounds like the ergonomics effort is (partly) for newbies like me, so I'm trying to express my struggles in terms of a pattern that the Rust team could optimize for. It seems like something that people would hit quite often. I'm sure there is an answer to what I'm trying to do, so my point is that maybe it should be easier to find, or at least better documented. |
Possibly I've missed something critical about your example, but I think you may want to create a struct Log, turn open_log() into Log::new(), and put the things the log needs (such as the log file) inside Log, owned by Log.