In the
previous section, we analyzed the database configuration file to read the connection parameters from it.
Now let's implement directly the database update operations: creating, updating, deleting our records and the corresponding command line interface.
First, let's look at the arguments of the program. Its interface will look like this:
const HELP: &'static str = "Usage: phonebook COMMAND [ARG]... Commands: add NAME PHONE - create new record; del ID1 ID2... - delete record; edit ID - edit record; show - display all records; show STRING - display records which contain a given substring in the name; help - display this help.";
There are already a couple of interesting points here.
const declares a constant, and such that it is simply built into the place of use. Thus, it does not have its own address in memory - it looks like #define in C. The type of constant must always be specified - and in this case it may look a little scary. & 'static str? What is it?
')
If my memory serves me, we have not yet seen the clearly indicated lifetimes. So, this is a link, & str, and it can be written differently as & 'foo str. Usually we do not have to explicitly specify the time of life, because the compiler itself can output it - i.e. 'foo just goes down.
I also note that 'foo could be' bar or anything else — it's just the name of a variable. In our case, you can think of it like this: link HELP: & str has a lifetime called 'foo, and it is equal to' static.
Now about 'static. This is a lifetime equal to the lifetime of the program. Our line is directly embedded in the image of the program, and it does not require any initialization or explicit destruction. Therefore, it is always available while the program is running. You can read more about 'static
here .
Thus, we declared a string constant, which is always available.
But the code for parsing the arguments - as always, first of all. Then we take a closer look at it.
Command line parsing code let args: Vec<String> = std::env::args().collect(); match args.get(1) { Some(text) => { match text.as_ref() { "add" => { if args.len() != 4 { panic!("Usage: phonebook add NAME PHONE"); } let r = db::insert(db, &args[2], &args[3]) .unwrap(); println!("{} rows affected", r); }, "del" => { if args.len() < 3 { panic!("Usage: phonebook del ID..."); } let ids: Vec<i32> = args[2..].iter() .map(|s| s.parse().unwrap()) .collect(); db::remove(db, &ids) .unwrap(); }, "edit" => { if args.len() != 5 { panic!("Usage: phonebook edit ID NAME PHONE"); } let id = args[2].parse().unwrap(); db::update(db, id, &args[3], &args[4]) .unwrap(); }, "show" => { if args.len() > 3 { panic!("Usage: phonebook show [SUBSTRING]"); } let s; if args.len() == 3 { s = args.get(2); } else { s = None; } let r = db::show(db, s.as_ref().map(|s| &s[..])).unwrap(); db::format(&r); }, "help" => { println!("{}", HELP); }, command @ _ => panic!( format!("Invalid command: {}", command)) } } None => panic!("No command supplied"), }
Let's look at the first line:
let args: Vec<_> = std::env::args().collect();
std :: env :: args () simply returns an iterator over the command line arguments. Why is this an iterator and not some kind of static array? Because we may not need all the arguments, and potentially there may be a lot of them. Therefore, an iterator is used - it is "lazy." This is in the spirit of Rust - you do not pay for what you do not need.
So, here we obviously have few arguments and it will be easier for us to have a normal vector from which the arguments can be taken by indices. We do .collect () to bypass all the elements and collect them into a specific collection.
What kind of collection? Here is a subtle point. In fact,
.collect () calls the
from_iter () method of the collection in which the elements are put. So, we need to know its type. That is why we cannot omit the args type and write this:
let args = std::env::args().collect();
Here is what the compiler will say to this:
main.rs:61:9: 61:13 error: unable to infer enough type information about `_`; type annotations or generic parameter binding required [E0282] main.rs:61 let args = std::env::args().collect(); ^~~~ main.rs:61:9: 61:13 help: run `rustc
However, notice that type inference does its job: we only need to specify Vec <_> as the type: what type lies in the vector, the compiler already knows. It is only necessary to clarify which collection we want.
Well, why all these difficulties? So that we can, for example, collect arguments into a linked list (or some other collection), if we want:
let args: std::collections::LinkedList<_> = std::env::args().collect();
A list of collections that implement from_iter is on the
type documentation page.
Next we see:
match args.get(1) {
.get () returns Ok (element) if an element of the vector exists, and None otherwise. We use this to detect a situation where the user does not specify a command:
} None => panic!("No command supplied"), }
If the command does not match any of the predefined ones, we output an error:
command @ _ => panic!( format!("Invalid command: {}", command))
We want to get into this branch for any value of text — therefore, _, “any value” is used as the value of this branch. However, we want to output this very wrong command, so we associate the match expression with the name command with the command @ _ construct. Read more about this syntax
here and
here .
Further analysis looks like this:
Some(text) => { match text.as_ref() { "add" => {
If we have a command, we will get into the Some (text) branch. Next, we use match again to match the name of the team - as you can see, match is quite universal.
The teams understand quite the same way, so let's consider the most interesting: delete. It accepts a list of record identifiers that must be deleted.
"del" => { if args.len() < 3 { panic!("Usage: phonebook del ID..."); } let ids: Vec<i32> = args[2..].iter() .map(|s| s.parse().unwrap()) .collect(); db::remove(db, &ids) .unwrap(); },
First we need identifiers: we get them from the command line arguments as follows:
let ids: Vec<i32> = args[2..].iter() .map(|s| s.parse().unwrap()) .collect();
With let foo: Vec <_> = ... .collect (), we are already familiar. It remains to understand what is happening inside this line.
args [2 ..] gets a slice of the vector - starting from the third element to the end of the vector. Looks like slices in python.
.iter () gets an iterator over this slice, to which we apply an anonymous function using .map ():
.map(|s| s.parse().unwrap())
Our anonymous function takes a single argument, s, and parses it as an integer. How does she know that it must be a whole? From here:
let ids: Vec<i32> =
(Hehe, in fact, not even from here, but from the signature of the db :: remove function - it accepts the & [i32] slice. Type output uses this information to understand that
FromStr :: from_str should be called from i32. Therefore we could also use Vec <_> here, but for documenting the code, we explicitly indicated the type. About db :: remove itself - below.)
In general, using iterator adapters like .map () is a common pattern in Rust code. It allows you to get controlled laziness of execution where it is most often needed - when streaming reading some data.
Great, we coped with all the preparatory work. It remains to update the base itself. insert looks
quite boring . Let's look at remove.
By the way, why is it written as db :: remove? Because it is in a separate module. At the file level, this means that it is in a separate source: src / db.rs. How is this module included in our main file? Like this:
mod db;
Simply! This instruction is equivalent to inserting the entire source code of a module in the place where it is written. (But in fact this does not happen, it’s not a preprocessor. The entire container is compiled here at once, so the compiler can read the modules into memory and establish connections at the intermediate presentation level, and not stupidly copy the source code as text.) That the compiler will look for the module in the src / db.rs and src / db / mod.rs files - this allows you to accurately organize the hierarchy of modules.
Now the code of our function:
pub fn remove(db: Connection, ids: &[i32]) -> ::postgres::Result<u64> { let stmt = db.prepare("DELETE FROM phonebook WHERE id=$1").unwrap(); for id in ids { try!(stmt.execute(&[id])); } Ok(0) }
So, here we know almost everything. In order.
pub means that the function is available outside the module. Otherwise, we would not be able to call it from main, since By default, all functions inside modules are hidden:
main.rs:81:21: 81:31 error: function `remove` is private main.rs:81 db::remove(db, &ids) ^~~~~~~~~~
The return type looks weird. :: postgres :: Result?
Two colons mean that the postgres module needs to be searched from the root of our container, and not from the current module. This module is automatically declared in main.rs when we do extern crate postgres. But it does not become visible in db.rs automatically! Therefore, we climb into the root of the namespace with :: postgres. We could also re-request the binding of the postgres container in db.rs, but this is not considered good practice - it’s better if all the binding requests are in the same place and the other modules use what is available in the main.
Well, we figured out a bit with the modules. See
here for more details.
Next, we see an unprecedented macro:
try!
.
He, as his name suggests, is trying to perform an operation. If it succeeds, the value of try! () Will be the value nested in Ok (_). If not, it performs something similar to return Err (error). This is an alternative to our constant .unwrap () - now the program does not panic in case of an error, but returns an error to the top for processing by the calling function.
This macro can be used in functions that return Result themselves - otherwise the macro will not be able to return Err, since return type and value type in return do not match.
With the removal of everything. Next, I selectively go through the rest of the operations, describing what we do not yet know.
Here, for example, how to work with transactions:
{ let tx: ::postgres::Transaction = db.transaction().unwrap(); tx.execute( "UPDATE phonebook SET name = $1, phone = $2 WHERE id = $3", &[&name, &phone, &id]).unwrap(); tx.set_commit(); }
As you can see, this is a typical application of RAII. We simply don’t pass tx anywhere, and it is destroyed upon exiting the block. The implementation of its destructor saves or rolls back the transaction depending on the success flag. If we had not done tx.set_commit (), the tx destructor
would have rolled it back .
And here is how you can format a string without printing it on the screen:
Some(s) => format!("WHERE name LIKE '%{}%'", s),
When we create a vector, you can immediately specify how many elements it should allocate memory:
let mut results = Vec::with_capacity(size);
And finally, another functional code example:
let max = rs.iter().fold( 0, |acc, ref item| if item.name.len() > acc { item.name.len() } else { acc });
This code could be written easier if we compared the types for which the
Ord type is implemented:
let max = rs.iter().max();
Or, we can implement this
type for Record. It requires the implementation of PartialOrd and Eq, and Eq, in turn, is PartialEq. Therefore, in fact, have to implement 4 types. Fortunately, the implementation is trivial.
Implementation of types use std::cmp::Ordering; impl Ord for Record { fn cmp(&self, other: &Self) -> Ordering { self.name.len().cmp(&other.name.len()) } } impl PartialOrd for Record { fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.name.len().cmp(&other.name.len())) } } impl Eq for Record { } impl PartialEq for Record { fn eq(&self, other: &Self) -> bool { self.id == other.id && self.name == other.name && self.phone == other.phone } } pub fn format(rs: &[Record]) { let max = rs.iter().max().unwrap(); for v in rs { println!("{:3} {:.*} {}", v.id, max.name.len(), v.name, v.phone); } }
It is worth noting that the meaningfulness of such an implementation is questionable - it is hardly worth comparing database records along the length of one of the fields.
By the way, the Eq type is one of the examples of marker types: it does not require the implementation of any methods, but simply tells the compiler that some type has a certain property. Other examples of such types are Send and Sync, about which we will talk more.
For today everything is a post, and so it turned out to be the longest of the series.
Now our application really works, but it does not have a REST interface yet. The web part we will do next time.