How to Create a Global, Mutable Vector

Hello Rust Community!

I'm trying to create a ToDo Application, for now, it's a simple task app that has a single struct (task).

Ask: I need to start keeping the full list of tasks somewhere, so the user can query the full list of tasks, and eventually filter them. However, I'm having trouble creating something that the compiler allows.

I'm new to rust in general, so thank you for your patience

pub mut all_tasks = vec![];

pub struct Task {
    name: String,
    completed: bool,
    priority: u8, //will be 1-5, 1 being highest
}

pub fn create_task() -> Task {
    let new_task = Task {
        name: String::from("Test Task"),
        completed: false,
        priority: 5,
    };
    all_tasks.push(new_task);
    return new_task;
}

As you can see, i'm trying a very simple format where I create a global vector (since it should only have tasks inside of it) and upon the creation of a new task, I want to be able to add the new task to the vector, and change the tasks by searching within this vector.

Am I just going about this wrong? or is there an easy way to get this format to work?

Thank you in advance for your patience and expertise.

1 Like

It’s usually a better idea idea to define a TaskList struct that contains your vector and the operate on it via the self parameter:

pub struct TaskList(Vec<Task>);

pub struct Task {
    name: String,
    completed: bool,
    priority: u8, //will be 1-5, 1 being highest
}

impl TaskList {
    pub fn create_task(&mut self) -> &Task {
        let new_task = Task {
            name: String::from("Test Task"),
            completed: false,
            priority: 5,
        };
        self.0.push(new_task);
        return &self.0[self.0.len()-1];
    }
}

Then, instead of it being a global, you can probably get away with it being a local variable in main that has references passed around your program. Among other things, this lets individual #[test] functions operate in parallel on their own copies of a TaskList.


If you then decide that you need to have a global task list available, you’ll be able to use something like the lazy_static or once_cell crates to create a global Mutex<TaskList> without needing to intersperse synchronization code with your main program logic.

1 Like

This is a wonderful solution. Thank you for your quick and thorough response!

Quick semantics question: why do you use the 0 in your reference to the vector? is that referring to the 0th index? or is this the 0th vector in the struct?

It’s how you refer to individual fields in tuples and tuple-like structs, which don’t have field names:

(42, "hello").0 == 42
(42, "hello").1 == "hello"
(42, "hello").2 // compile error

In this case, TaskList is defined to have a single anonymous field of type Vec<Task> which then gets accessed as task_list.0. You could as easily define it like this:

pub struct TaskList {
    tasks: Vec<Task>
}

and then replace every self.0 with self.tasks. The final generated code should be the same.

1 Like

It's generally better to encode something like that in an enum, especially if the range of valid values is so low.

#[repr(u8)] // To have the same layout as a u8
#[derive(Debug)] // Enable pretty printing (optional)
enum Priority {
    Highest = 1,
    High = 2,
    Medium = 3,
    Low = 4,
    Lowest = 5,
}

This both prevents unexpected values in your code and it allows to pretty print the values easily.

If you need to work with the priorities as a u8, you can easily convert them to that:

let x = Priority::Low;
println!("{}", x as u8);
// prints "4"

Converting any u8 to a Priority would be a little different, since that conversion could fail in the general case. You could do it like this, if you need that:

use std::convert::TryFrom;

impl TryFrom<u8> for Priority {
    type Error = ();

    fn try_from(val: u8) -> Result<Priority, Self::Error> {
        Ok(match val {
            1 => Priority::Highest,
            2 => Priority::High,
            3 => Priority::Medium,
            4 => Priority::Low,
            5 => Priority::Lowest,
            _ => return Err(()),
        })
    }
}

and use it like this

use std::convert::TryInto;

let x: Priority = 1u8.try_into().unwrap();
// this unwrap crashes the program, if the value is not in the range 1-5
println!("{:?}", x);
// prints "Highest"
1 Like

For the sake of your sanity in Rust, and in programming in general in any language, it is better that you expunge the idea of 'global' from your mind altogether.

You can wrap all your data and the functions that operate on it in a struct and make those functions methods of the struct with impl. With the slight inconvenience that you now need to refer to what may have been globals with self.whatever

Which I guess is what has been suggested above.

My only little point is that setting out to create a global anything it is almost always the wrong way to think about whatever problem you have and creating globals is not the problem you actually want to solve anyway.

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