This post is part of a series. Links to all parts below.
- Part 1 - Introduction to Rust
- Part 2 - Can I borrow that?
- Part 3 - Crates, Modules and the web
Ownership
Hopefully part 1 gave you a pretty good first impression of Rust, and enticed you to want to learn more. But to keep it reasonably short I had to gloss over some subjects, and skip mentioning some completely. If you try to experiment with the code from part 1 yourselves without reading up on Rust you are likely to run into this message error: use of moved value: x
.
This is one of the most foundational concepts in Rust, Ownership
, where at any point in your program any resource is owned by exactly one thing. And a resource can either be moved
(change owner), be borrowed
or be copied
.
Let's have a look at how this all works with some code.
// Let's reuse our Todo struct from last time
struct Todo {
id: i16,
title: String,
completed: bool,
deleted: bool,
}
// Simple function to print a todo, here Rust will default to moving ownership
// of the todo to the function
fn print_todo(todo: Todo) {
println!("Todo item {}: {}", todo.id, todo.title);
}
fn main() {
// Create a todo item
let item0 = Todo {
id: 0,
title: "Borrow something".to_string(),
completed: false,
deleted: false
};
// Print todo, and move ownership away from `item0`
print_todo(item0);
// Try to assign item0 to another variable (this will cause a `use of moved value` error)
let borrow_something = item0;
}
When a resource is moved
ownership is transfered, so in our example the ownership of the resource item0
represents is moved to the print_todo
function. And when we try to use item0
again is no longer linked to that resource.
So how do we fix it? This is where borrowing comes in:
// Now the function takes the borrowed version `&Todo`
fn print_todo(todo: &Todo) {
println!("Todo item {}: {}", todo.id, todo.title);
}
fn main() {
// ...
// Print todo, borrowing the item0 value
print_todo(&item0);
// No error here
let borrow_something = item0;
}
That was easily solved by letting print_todo
borrow &item0
. The ampersand signifies borrowing in Rust and makes sure we only get a reference to the value.
Copyable types
Borrowing is great, but there are times when we don't need to borrow or move values, I'm talking about copyable types:
fn main() {
let maximum_todos = 10;
let max = maximum_todos;
// Integers are a `copyable` type, which means by default no ownership is involved
// The value is simply copied
println!("maximum_todos is still valid: {}", maximum_todos);
}
So this code runs, because integers are copyable.
Primitive types in Rust will by default be copied when used in assignments or passed to functions. You see very similar behaviour in JavaScript where primitive types like number
, boolean
and string
are copied:
var a = 7;
var b = a;
a = 9;
console.log(b);
will print 7
because b gota copy of the value in *a at the time it was copied and there is no further link to the a variable binding. If we look at object
or array
instead, we see a different behaviour:
var a = { name: 'Fredrik' };
var b = a;
a.name = 'Andersson';
console.log(b);
will print Object {name: "Andersson"}
because b got a reference to the object in a instead of a copy of it, and changes to that object will affect both a and b.
Back to Rust.
Let's look at the primitives that are copied by default so that we know when there is no need to borrow a reference, there will be a few types here that I did not cover in part 1.
fn main() {
// All numeric primitives, signed and unsigned are copyable
let a: i64 = 10;
let b = a;
let c: f32 = 0.1;
let d = c;
println!("a and c are not moved: {} {}", a, c);
// Bools too
let e = true;
let f = e;
println!("Bools are copyable too: {}", e);
// Char's and string slices are copyable too, BUT NOT Strings
let g: char = 'g';
let h = g;
let i = "string slices are copyable";
let j = i;
println!("chars are copyable: {} aaaand {}", g, i);
// Arrays and array slices too
let k = [1, 2, 3];
let l = k;
// Get a slice of the first entry in the k array
let m = &k[0..1];
let n = m;
println!("Arrays and array clices are copyable: {:?} {:?}", k, m);
// Tuples too
let o = (1, "str");
let p = o;
println!("Tuples are copyable {:?}", o);
// That was the types that are copyable by default
// We can also implement Copy on our own types as long
// as they hold copyable types themselves
struct Point {
x: i32,
y: i32,
}
impl Copy for Point {}
impl Clone for Point { fn clone(&self) -> Point { *self } }
let q = Point { x: 1, y: 10 };
let r = q;
println!("We made a copyable struct! {}/{}", q.x, q.y);
// Phew! that was some black magic. Copy and Clone are `traits` that are built into the language
// Traits are basically interfaces that you can implement on your types
// You can create them yourselves too, which we'll get to in a future post
// There is an easier way to make a type copyable though:
#[derive(Copy, Clone, Debug)]
struct Point3D {
x: i32,
y: i32,
z: i32,
}
let s = Point3D { x: 1, y: -20, z: 30 };
let t = s;
println!("Point3D is now copyable, and the Debug trait let's us print it easily {:?}", s);
}
As I mentioned a few of these types may be new to you, first we have Char
which represents a single unicode scalar value.
The second new type is the Tuple
which is a very interesting collection type, you can stick an unlimited number of comma separated values into parens, and then access them by pattern matching, or by index. They are very handy when you want to return more than one thing from a function.
Also new is the ability to derive traits, like the Copy trait to make a struct
copyable. Traits in general are incredibly useful and an awesome language feature deserving of their own chapter.
This long list of primitive examples hopefully gets the point across that far from all types in Rust will be moved when you assign them to another variable, or pass them to a function.
Borrowing
Let's look at a few more examples of borrowing:
#[derive(Clone, Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let a = Point {
x: 0,
y: 0,
};
// As discussed this would cause a `moved value` error
//let b = a;
//let c = a;
// Borrowing is fine an unlimited amount of times though
let b = &a;
let c = &a;
// But only for as long as the reference lives
// Rust is block scoped, basically any time you see {} it's a block
{
let d = Point { x: 0, y: 0 };
// This is totally fine
let e = &d;
// But here, at the end of this block, d goes out of scope
}
// So this would fail
//let f = &d;
// You can also borrow a value mutably, but then the borrower has
// exclusive rights to mutate the value until it goes out of scope
let mut f = Point { x: 0, y: 0 };
let g = &mut f;
// This would fail
//let h = &mut f;
// Even the owner can't mutate itself without causing an error
//f.x = 1;
// This means that mutable borrows are mostly practical when passing something to a function
fn pointifier(point: &mut Point, multiply_by: i32) {
point.x *= multiply_by;
point.y *= multiply_by;
}
let mut h = Point { x: 1, y: 2 };
pointifier(&mut h, 20);
// once the pointifier function has finished the borrow ends and h is mutable again
h.x -= 1;
println!("Point {:?}", h);
}
That's the basics of it, you might be wondering why someone would impose these kinds of restrictions on a language right now...
This ownership concept is a big part of the secret sauce that lets Rust be both ridiculously fast and memory safe. To explain it we need to go into a topic most JavaScript programmers do not spend a great deal of time thinking about:
Memory management
To make any program work, memory must be allocated for it's resources, and deallocated when a resource is no longer needed, when the latter doesn't happen, we have a memory leak.
Over the years different strategies have evolved to handle this. JavaScript, along with Java, Ruby and many other popular languages, is garbage collected
. This means that there is a process that, at runtime, will keep track of what allocated memory you aren't using anymore and will automatically deallocate it for you.
That sounds great, right? Well, yes. The garbage collectors in these languages are great, they have been optimized over many years, by really smart people with feedback from huge communities. But still, it's a process that will periodically suspend execution of your program to clean up your mess best it can, impacting performance.
Coming from JavaScript, memory management is not something you are forced to worry about (though it is extremely easy to create memory leaks when writing JS). In something like C and C++ you are forced to not only think about it, but you spend a lot of time allocating and deallocating memory manually. Rust takes a middle ground here.
The ownership system makes Rust programs very easy to reason about for the compiler, resources in memory are simply deallocated when the one owner goes out of scope. Combined with a concept called lifetimes
, that we will cover later, developers get fine grained control of memory management without manually having to dealloce memory. The bigger benefit is that Rust's compiler/borrow checker will at compile time make sure that your code is memory safe.
So by not having a garbage collector, but instead through some very well designed constraints allow the compiler to reason about your program to the extent that it can write better memory management code than you would (or at least I would), Rust can be safe and fast.
The todo list
So using what we now know about borrowing, can we improve upon the structure we created last time? I'm pretty sure we can. Our todo list is the application state, and one of the nicest way to handle state in Javascript is Redux.
If you don't know what redux is you can read some of their awesome documentation here, or just accept that what we are going to build is pretty close.
Let's look at what we need to implement something like Redux:
State
// Ripping off the canonical Redux todo example we'll add a
// visibility filter to our state in addition to the todos we already had
// This state struct will be the single source of state for our todo list program
#[derive(Clone, Debug)]
struct State {
todos: Vec<Todo>,
visibility_filter: VisibilityFilter
}
impl State {
// Can be called with State::default()
fn default() -> State {
State {
todos: Vec::new(),
visibility_filter: VisibilityFilter::ShowAll,
}
}
}
So, there are a few things to cover in the above code:
We define a State struct that will hold our application state. The state is a list of Todo objects (structs), the same we have used before. And a new field to hold a VisibilityFilter
. This will be of the type enum
.
Enums are a special user defined type that can hold data that is one of a predefined number of alternatives. Our VisibilityFilter will look like this:
enum VisibilityFilter {
ShowActive,
ShowAll,
ShowCompleted,
}
So the visibility_filter field in our state will always be one of those 3 options. And they will control what todo items we show in the list.
You can also see how easy it is to attach methods to a struct, we add a function named default
to the State, which will let us call State::default() to get a new state with an empty todo list and the visibility_filter set to ShowAll.
This is also the first time we define a function that returns something. In Rust the type a function returns needs to be defined, and it's done by adding -> and the type before the opening brackets of the function body: fn default() -> State {
. So this function clearly returns a State
.
Actions
Redux has the concept of actions (action objects) which in JavaScript is just an object with a mandatory property type
so for example: { type: "ADD_TODO", title: "Buy milk" }
.
When actions are dispatched on a Redux store, it will run a function called a reducer, that takes the current state and the action object, and after modifying a copy of the state according to the action returns the new state.
A common pattern in Redux is to create functions that create these action objects. These functions are called action creators
.
function addTodo(text) {
return { id: generate_id(), text }
}
Rust has enums, so the enum type can replace the "type" property of Redux objects.
The enums will replace action creators
too since they can be used as constructors like this: Todos(Add("Todo item".to_string()))
.
The full code for actions will look like this, note that we are deriving
2 traits on these: Clone
that will let us call .clone()
on these enums, and Debug
that will let us print out their values easily.
#[derive(Clone, Debug)]
enum Action {
Todos(TodoAction),
Visibility(VisibilityFilter),
}
// mark_done from the previous example becomes Toggle to align with the Redux example
// otherwise functionality is the same
#[derive(Clone, Debug)]
enum TodoAction {
Add(String),
Toggle(i16),
Remove(i16),
}
// Our 3 visibility states
#[derive(Clone, Debug)]
enum VisibilityFilter {
ShowActive,
ShowAll,
ShowCompleted,
}
Reducers
I mentioned that when an action is dispatched a function called a reducer is run. Ours will look like this:
fn reducer(state: &State, action: Action) -> State {
// Always return a new state
State {
todos: todo_reducer(&state.todos, &action),
visibility_filter: visibility_reducer(&state.visibility_filter, &action),
}
}
Here we can see some borrowing in action. Our reducer borrows a reference to the current state, and returns a new state. It gets this state by delegating the work to 2 functions we haven't defined yet.
todo_reducer
will set the todos
field of the state, the reducer
passes on a borrowed reference to the current todos, and the action. If the action is related to todos the todo_reducer will returned a modified todo list, otherwise it will return a copy of the same state that it was passed.
visibility_reducer
will set the visibility_filter
field in very much the same way.
This is a little dirty, the main reducer just has to know the names of the child reducers for this to work. Redux provides a combineReducers
function that would let us build the main reducer function, by passing in the child reducers. However, to recreate that in Rust would bring us into topics that we aren't going to cover in this post.
With that said, let's have a look at how our 2 child reducers are implemented:
// Helper function for getting a mutable todo from a vector by todo_id
fn get_mut_todo(todos: &mut Vec<Todo>, todo_id: i16) -> Option<&mut Todo> {
todos.iter_mut().find(|todo|todo.id == todo_id)
}
// Our todo reducer, takes in state (todo list) and returns a new/cloned version
// after applying the action (is applicable)
fn todo_reducer(state: &Vec<Todo>, action: &Action) -> Vec<Todo> {
let mut new_state: Vec<Todo> = state.clone();
// First we make sure it's a `Todos` action, otherwise return clone of incoming state
match *action {
Todos(ref todo_action) => match *todo_action {
// Pretty simple from here on, check the type of Todos enum type
// If Add push a new item, and if `Toggle` or `Remove` use our get_mut_todo
// helper function and then change a property on the todo
Add(ref title) => {
let new_id = new_state.len() as i16 + 1;
new_state.push(Todo::new(new_id, title.to_string()))
},
Toggle(todo_id) => {
if let Some(todo) = get_mut_todo(&mut new_state, todo_id) {
if todo.completed { todo.completed = false; } else { todo.completed = true; }
}
},
Remove(todo_id) => {
if let Some(todo) = get_mut_todo(&mut new_state, todo_id) {
todo.deleted = true;
}
},
},
// If it's not a Todos action change nothing
_ => (),
}
return new_state;
}
// Very simple reducer since the action will either be a VisibilityFilter, in which
// case we will return that, otherwise just return the incoming state
fn visibility_reducer(state: &VisibilityFilter, action: &Action) -> VisibilityFilter {
match *action {
Visibility(ref vis_action) => vis_action.clone(),
_ => state.clone(),
}
}
Hopefully that is pretty clear. In the todo_reducer
we first clone the state, and then make use of Rusts match
statement for identifying the action. We first check if it was a Todos action, and if so what kind. The code for adding, toggling or deleting todos is pretty straight forward, and close to what we had in the last example.
We mutate the state in this function, but we mutate a cloned state, so the state that is returned is always a new copy.
For the visibility_reducer
an action of the type VisibilityFilter
is also the value we want in our state, so it will return a clone of the action if it was a VisibilityFilter type, or a clone of the current state otherwise.
Store
The last big Redux concept missing is the store
, it's the piece that ties everything together. It holds the state, and let's us subscribe to state changes, and dispatch actions to update the state.
struct Store {
state: State,
listeners: Vec<fn(&State)>,
reducer: fn(&State, Action) -> State,
}
The state
field will hold the current state, the listeners
field will hold references to any functions that subscribed to our store, and the reducer
field will hold the reducer function that is passed in to create the store.
Let's have a look at the methods of the Store:
impl Store {
// Takes a reducer function as the only argument
// To keep it simple, State::default() provides the initial state in this eample
fn create_store(reducer: fn(&State, Action) -> State) -> Store {
Store {
state: State::default(),
listeners: Vec::new(),
reducer: reducer,
}
}
// Pushes a listener that will be called for any state change
fn subscribe(&mut self, listener: fn(&State)) {
self.listeners.push(listener);
}
// Simply returns a borrowed reference to the state
#[allow(dead_code)]
fn get_state(&self) -> &State {
&self.state
}
// Called for every new action, calls the reducer to update the state
// and then calls every listener with the new state
fn dispatch(&mut self, action: Action) {
self.state = (self.reducer)(&self.state, action);
for listener in &self.listeners {
listener(&self.state)
}
}
}
fn create_store(reducer: fn(&State, Action) -> State) -> Store {
looks a little complicated, but it's only a function that takes a single function (the reducer) as it's only argument, and returns a Store object. Because Rust is strongly typed we need to specify the type of the function we pass in: reducer: fn(&State, Action) -> State
.
You can check that this matches the reducer we defined earlier, it's a function that takes a borrowed reference to the state, and an action, and returns a state.
create_store
then returns a Store struct with the default state, an empty vector of listeners and the reducer we just passed in.
subscribe
simply pushes the incoming listener function to the Store's listeners
vector. Note the function signature on the listener listener: fn(&State)
, this means any functions passed to subscribe need to take exactly one argument, a borrowed State.
get_state
is a method to simply grab the current state, it returns a borrowed reference to the state. Note the #[allow(dead_code)]
above the function definition. This turns off the Rust compiler's friendly warnings about us not using this code. We keep this method since it's part of the Redux spec.
dispatch
is probably the most interesting. You can see it takes a mutable borrow &mut self
, this is because it will update the current state by calling the reducer and saving the returned state as the new state.
Once that's done it loops through the listeners and calls them with the new state, this time passing along an immutable borrowed reference to the state.
Alright! We have all the building blocks needed for state management. Let's look at the output functions we have for printing our list and instructions:
// Very simple function to print a todo
fn print_todo(todo: &Todo) {
let done = if todo.completed { "✔" } else { " " };
println!("[{}] {} {}", done, todo.id, todo.title);
}
// Our print_todos function from last time, a bit altered to take State
// as input instead of a todos list directly
fn print_todos(state: &State) {
let visibility = &state.visibility_filter;
println!("\n\nTodo List:\n-------------------");
for todo in &state.todos {
if !todo.deleted {
match *visibility {
ShowAll => print_todo(&todo),
ShowCompleted => if todo.completed { print_todo(&todo) },
ShowActive => if !todo.completed { print_todo(&todo) },
}
}
}
println!("-------------------\nVisibility filter: {:?}", visibility);
print_instructions();
}
fn print_instructions() {
println!("\nAvailable commands: \nadd [text] - toggle [id] - remove [id]\nshow [all|active|completed]");
}
fn invalid_command(command: &str) {
println!("Invalid command: {}", command);
}
A few things have changed here. print_todos
is now visibility_filter aware. It will print the todos only for the matching filter. We've broken out printing of a single todo item to a print_todo
function so that it's easy to call it in several branches of our match statement.
Matching on enums is very straight forward as you can see. We also added a simple function to print the instructions for using the todo list.
So what about the actual implementation of all this?
let mut store = Store::create_store(reducer);
store.subscribe(print_todos);
print_instructions();
Unlike earlier, where we just made a todo list and then started modifying it, we now create a store, and subscribe our print_todos
function to it. This means the function will be called with the new state whenever it is updated, so we always have an up to date list printed.
// Same input handling as last time, the interesting parts will be in our match statement
loop {
let mut command = String::new();
io::stdin()
.read_line(&mut command)
.expect("failed to read line");
let command_parts: Vec<&str> = command.split_whitespace().collect();
match command_parts.len() {
0 => invalid_command(&command),
_ => {
match command_parts[0] {
// Since we prepared so well we just need to call dispatch on our store
// With the right action
"add" => store.dispatch( Todos(Add( command_parts[1..].join(" ").to_string() ))),
"remove" => if let Ok(num) = command_parts[1].parse::<i16>() {
store.dispatch( Todos(Remove(num)));
},
"toggle" => if let Ok(num) = command_parts[1].parse::<i16>() {
store.dispatch( Todos(Toggle(num)));
},
"show" => match command_parts[1] {
"all" => store.dispatch( Visibility(ShowAll) ),
"active" => store.dispatch( Visibility(ShowActive) ),
"completed" => store.dispatch( Visibility(ShowCompleted) ),
_ => invalid_command(&command)
},
_ => invalid_command(&command),
}
},
}
}
We get the commands very much like in the previous example, we still split the terminal commands on spaces, and then handle the parts.
Once we have matched the command to a known action we simply run store.dispatch
with the action, otherwise we run the invalid_command function to print out the faulty command to the user.
Let's look at the add action: "add" => store.dispatch( Todos(Add( command_parts[1..].join(" ").to_string() ))),
. This creates and dispatched a Todos enum, with an Add enum as a value, that in turn holds a String as it's value, the string being any input that follows the add
command.
That's it, we've passed around a bunch of borrowed references, and our todo list still works as before, even with a few new features. The full code now looks like this:
use std::io;
// Lets us type `Add("Todo item".to_string())` instead of `TodoAction::Add("Todo item".to_string())`
use TodoAction::{ Add, Remove, Toggle };
// Same with the Action enum and VisibilityFilter, Action::*; would work too, but this way we list what we use
use Action::{ Todos, Visibility };
use VisibilityFilter:: { ShowActive, ShowAll, ShowCompleted };
// Ripping off the canonical Redux todo example we'll add a
// visibility filter to our state except for the todos we already had
#[derive(Clone, Debug)]
struct State {
todos: Vec<Todo>,
visibility_filter: VisibilityFilter
}
// By implementing a struct we are creating something very much like
// a class, we can attach methods to it refering to &self` or `&mut self`
impl State {
// This gives us a quick way to initialize a default state with State::default()
pub fn default() -> State {
State {
todos: Vec::new(),
visibility_filter: VisibilityFilter::ShowAll,
}
}
}
// Same Todo as last time..
#[derive(Clone, Debug)]
struct Todo {
id: i16,
title: String,
completed: bool,
deleted: bool,
}
// Create a convenient Todo::new(id, title) method
impl Todo {
pub fn new(id: i16, title: String) -> Todo {
Todo {
id: id,
title: title,
completed: false,
deleted: false,
}
}
}
// Rust has enums, so the enum type can replace the "type" property of Redux objects
// The enums will replace `action creators` too since Todos(Add("Todo item".to_string()))
// is pretty clear
#[derive(Clone, Debug)]
enum Action {
Todos(TodoAction),
Visibility(VisibilityFilter),
}
// mark_done from the previous example becomes Toggle to align with the Redux example
// otherwise functionality is the same
#[derive(Clone, Debug)]
enum TodoAction {
Add(String),
Toggle(i16),
Remove(i16),
}
// Our 3 visibility states
#[derive(Clone, Debug)]
enum VisibilityFilter {
ShowActive,
ShowAll,
ShowCompleted,
}
// Helper function for getting a mutable todo from a vector by todo_id
fn get_mut_todo(todos: &mut Vec<Todo>, todo_id: i16) -> Option<&mut Todo> {
todos.iter_mut().find(|todo|todo.id == todo_id)
}
// Our main reducer, returns a new State with the results of the child-reducers
// No combineReducers is implemented here, so it calls the child reducers
// by function name
fn reducer(state: &State, action: Action) -> State {
// Always return a new state
State {
todos: todo_reducer(&state.todos, &action),
visibility_filter: visibility_reducer(&state.visibility_filter, &action),
}
}
// Our todo reducer, takes in state (todo list) and returns a new/cloned version
// after applying the action (is applicable)
fn todo_reducer(state: &Vec<Todo>, action: &Action) -> Vec<Todo> {
let mut new_state: Vec<Todo> = state.clone();
// First we make sure it's a `Todos` action, otherwise return clone of incoming state
match *action {
Todos(ref todo_action) => match *todo_action {
// Pretty simple from here on, check the type of Todos enum type
// If Add push a new item, and if `Toggle` or `Remove` use our get_mut_todo
// helper function and then change a property on the todo
Add(ref title) => {
let new_id = new_state.len() as i16 + 1;
new_state.push(Todo::new(new_id, title.to_string()))
},
Toggle(todo_id) => {
if let Some(todo) = get_mut_todo(&mut new_state, todo_id) {
if todo.completed { todo.completed = false; } else { todo.completed = true; }
}
},
Remove(todo_id) => {
if let Some(todo) = get_mut_todo(&mut new_state, todo_id) {
todo.deleted = true;
}
},
},
// If it's not a Todos action change nothing
_ => (),
}
return new_state;
}
// Very simple reducer since the action will either be a VisibilityFilter, in which
// case we will return that, otherwise just return the incoming state
fn visibility_reducer(state: &VisibilityFilter, action: &Action) -> VisibilityFilter {
match *action {
Visibility(ref vis_action) => vis_action.clone(),
_ => state.clone(),
}
}
// Redux store implementation
struct Store {
state: State,
listeners: Vec<fn(&State)>,
reducer: fn(&State, Action) -> State,
}
impl Store {
// Takes a reducer function, we skip the initial_state and optional arguments
// TO keep it simple, State::default() from earlier is our initial_state implementation
fn create_store(reducer: fn(&State, Action) -> State) -> Store {
Store {
state: State::default(),
listeners: Vec::new(),
reducer: reducer,
}
}
// Pushes a listener that will be called for any state change
fn subscribe(&mut self, listener: fn(&State)) {
self.listeners.push(listener);
}
// Simply returns the state
#[allow(dead_code)]
fn get_state(&self) -> &State {
&self.state
}
// Called for every new action, calls the reducer to update the state
// and then calls every listener
fn dispatch(&mut self, action: Action) {
self.state = (self.reducer)(&self.state, action);
for listener in &self.listeners {
listener(&self.state)
}
}
}
// Very simple function to print a todo
fn print_todo(todo: &Todo) {
let done = if todo.completed { "✔" } else { " " };
println!("[{}] {} {}", done, todo.id, todo.title);
}
// Our print_todos function from last time, a bit altered to take State
// as input instead of a todos list directly
fn print_todos(state: &State) {
let visibility = &state.visibility_filter;
println!("\n\nTodo List:\n-------------------");
for todo in &state.todos {
if !todo.deleted {
match *visibility {
ShowAll => print_todo(&todo),
ShowCompleted => if todo.completed { print_todo(&todo) },
ShowActive => if !todo.completed { print_todo(&todo) },
}
}
}
println!("-------------------\nVisibility filter: {:?}", visibility);
print_instructions();
}
fn print_instructions() {
println!("\nAvailable commands: \nadd [text] - toggle [id] - remove [id]\nshow [all|active|completed]");
}
fn invalid_command(command: &str) {
println!("Invalid command: {}", command);
}
fn main() {
// Let's create our store and subscribe with print_todos so every update is printed
let mut store = Store::create_store(reducer);
store.subscribe(print_todos);
print_instructions();
// Same input handling as last time, the interesting parts will be in our match statement
loop {
let mut command = String::new();
io::stdin()
.read_line(&mut command)
.expect("failed to read line");
let command_parts: Vec<&str> = command.split_whitespace().collect();
match command_parts.len() {
0 => invalid_command(&command),
_ => {
match command_parts[0] {
// Since we prepared so well we just need to call dispatch on our store
// With the right action
"add" => store.dispatch( Todos(Add( command_parts[1..].join(" ").to_string() ))),
"remove" => if let Ok(num) = command_parts[1].parse::<i16>() {
store.dispatch( Todos(Remove(num)));
},
"toggle" => if let Ok(num) = command_parts[1].parse::<i16>() {
store.dispatch( Todos(Toggle(num)));
},
"show" => match command_parts[1] {
"all" => store.dispatch( Visibility(ShowAll) ),
"active" => store.dispatch( Visibility(ShowActive) ),
"completed" => store.dispatch( Visibility(ShowCompleted) ),
_ => invalid_command(&command)
},
_ => invalid_command(&command),
}
},
}
}
}
Don't be surprised if you see more of the todo list, or a more complete Redux implementation in future posts.
Thanks for reading, if there's any feedback it would be greatly appreciated. Even if you just want to tell me how wrong I am about something. :)
Edit 2016-06-21: Updated for loops in dispatch
and print_todos
functions after some feedback from yanns (thanks for that :) )