Hacker News new | ask | show | jobs
by pjungwir 3400 days ago
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.

5 comments

> 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.

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.

So I passed out a lot of upvotes, but I thought I would add a thank you to you and others trying to help me. :-)

I will try your suggestion re the log. The database example is trickier I think since the prepared statements have references to `conn`, so it can't move. Also it's annoying that I have to make `negotiator` even when I don't need it.

It sounds like you're coming from the land of GC. Rust helps a lot with managing memory but the pattern you're talking about is creating garbage which the GC would then have to collect.

Manually collect the things you need to hold onto and put them into a struct and return that from open_log(); Do the same with prepare_db(). Then give the structs some methods for getting to the actual db object.

Alternatively use log4rs and rust-postgres. Or inspect their code to see how they handle it.

The answer is probably to return a value directly, although it's tough to say without a complete example.

But in general, if you have a routine that is creating stuff, and the stuff is meant for the caller to use, you create it in the routine and return it by value; the caller will then automatically "own" that value and either pass it somewhere else, or let it fall out of scope (which is when it'll be dropped and cleaned up for you).

It is not totally clear to me why you can't have those functions, or rather, those functions with a little bit of change to their signature. If you have this somewhere as a compilable example, I'm happy to look at it, but it's tough when there's so much stuff here that I don't know the signatures of.
I will certainly take you up on your offer!:

https://github.com/pjungwir/rust-initialization-functions

> I wish rust were smart enough to make the functions put the values directly in the caller's stack frame.

That's one reason why macros exist.

> 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.

Why can't you just "return" the variables you need later on ?