How to share code across code files?

Hello there,

I am quite new to Rust. I come from a C++ and Python background but I am very impressed with Rust and want to write code with it.
I am currently working through the Handbook, however, I have some questions.

Currently I am trying to write a program that loads data from a .csv file and loads it into a Postgres database.

I have the following structure:

Cargo.toml
Cargo.lock
src
- csv_operataions.rs
- data_structure.rs
- db_operations.rs
- main.rs

My main.rs looks like this:

use std::process;
use postgres::{Client, NoTls, Error};

pub mod csv_operations;
pub mod db_operations;
mod data_structure;
use data_structure::Record;

fn main() {
    println!("Try to connect to database.");
    let mut client: Client;
    db_operations::db_access(&mut client);

    println!("Try to read .csv file.");
    let mut vec: Vec<Record> = Vec::new();
    
    if let Err(err) = csv_operations::read_csv(&mut vec) {
        println!("Error while trying to read csv file: {}", err);
        process::exit(1);
    }

    // Here I would have a function, something like 
    db_operations::write_to_databse(&mut vec, &mut client);

}

My db_operations.rs looks like this:

use postgres::{Client, NoTls, Error};
use chrono::NaiveDate;

pub fn db_access(client: &mut Client) -> Result<(), Error> {
    client = &mut Client::connect("postgres://userpassword@localhost/database", NoTls)?;

    Ok(())

}

pub fn write_to_databse(vec: &mut Vec<Record>, client: &mut Client) -> Result<(), Error> {

    // Write data from vector into database

    Ok()
}

My csv_operations is something like this:

use std::error::Error;
use csv::ReaderBuilder;
use serde::Deserialize;
use super::data_structure::Record;
use postgres::Client;

pub fn read_csv(vec: &mut Vec<Record>) -> Result<(), Box<dyn Error>> {
    let path = "/path/with/file.csv";

    let mut reader = ReaderBuilder::new()
        .delimiter(b';')
        .from_path(path)?;

    for record in reader.deserialize() {
        let record: Record = record?;
        //vec.push(record);
    }

    Ok(())
}

The file data_structure.rs just defines the struct so I can make a vector from the csv file and write it into the database.

use serde::Deserialize;

#[derive(Deserialize)]
pub struct Record {
    #[serde(alias = "ID")]
    id: i32,
    ...

That does not work because main.rs declares client without initialising. In C++ I would have a header file that handles all of the database stuff and a header that handles all of the csv stuff and I would include that in my main file and then create the objects as needed and pass them to the right functions to get the work done, just to separate it logically.
How would I go about something like that in Rust? (the code is still incomplete because I'm currently working on it and I'm still questioning this approach)

Also both csv_operations.rs and db_operations.csv use the same data structure and so I include that with super::data_structure but is this really the way to go? I just want a simple struct to use across source files so I don't want to make an extra library.

Cheers,

Sorry but I have no idea why/how this is relevant or even what your problem is.

Rust has a real module system. You use modules instead of header files. There is no need for raw textual source splicing like #include – use statements bring items from modules into scope without the need of duplicating every declaration. If you have your Record in the data_structures module, you can already use it from multiple places.

"Initializing" (or not) a variable is an entirely different thing, unrelated to the module system and imports.

2 Likes

It's relevant because it doesn't compile because of that.

error[E0381]: used binding `client` isn't initialized
  --> src/main.rs:13:30
   |
12 |     let mut client: Client;
   |         ---------- binding declared here but left uninitialized
13 |     db_operations::db_access(&mut client);
   |                              ^^^^^^^^^^^ `client` used here but it isn't initialized

In Rust, one very rarely uses “out parameters” (pointers to be written to and not read) like you are trying to do here — and you cannot use uninitialized ones[1], which is why you got a compilation error. You should just return the Client instead:

pub fn db_access() -> Result<Client, Error> {
    let client = Client::connect("postgres://userpassword@localhost/database", NoTls)?;
    // any other code
    Ok(client)
}

Then the variable in main() can simply be initialized.

fn main() {
    println!("Try to connect to database.");
    let mut client = db_operations::db_access();

  1. without special tools and explicit unsafe code, which should be avoided if not necessary ↩︎

2 Likes

But that's not because of any imports. You have to initialize every variable before using it. If the compiler complains about a variable not being initialized, you'll only be able to fix that by initializing it — no amount of imports can help with that.

I do not claim that it has anything to do with imports and I realise that every variable has to be initialised, unless you are using unsafe Rust.
My question was in regards to what the (or a) correct way is to do something what I want to do in Rust. I will reiterate: when I want to split my program into logical parts, like establishing a connection to a database or reading a file and so want to use an object across multiple source files - what is the correct way to do that?
@kpreid already gave a hint.

Thank you, that is good to know. I had something like that in mind already but I did not realise that I could just replace the Result<(), Error> with Result<Client, Error> :slight_smile:

Indeed using parameters in a function as output is a practice that I and a lot of my work colleagues were constantly doing in C++.
Could you please give me a hint how one would proceed if I'd have two objects that I'd like to initialise in a function and wanted to pass both of them? Naively speaking I would guess that it's not very good programming practice and that you'd initialise each of the objects in its own functions and then you could easily pass them to a third function (which would not be a problem anymore since they are both initialised already) where you could do something interesting with them.

By the way - am I on the right track with how my code is structured? Would you do it differently? Like I said that is kinda how I'd do it in C++ since I'm not very used to Rust yet.

You mean, like returning a tuple or struct?

If the two values are linked in some way, then it can make sense to construct them in the same function and return them as a tuple. An example from the standard library is std::sync::mspc::channel(), which outputs both a Sender and Receiver that have to be created at the same time because they are opposite ends of the same channel. They can then be passed to other parts of the program separately.

There are certainly use cases for passing out parameters in using &mut references. The function std::io::Read::read() is an example, where a buffer is provided and the implementation can fill as much of the buffer as it is able to (the elements of the buffer are all still required to be intialised when it is passed in there, work on a reader interface with uninitialised buffers is ongoing).

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.