8

linuxfood has created bindings for sqlite3, for which I am thankful. I'm just starting to learn Rust (0.8), and I'm trying to understand exactly what this bit of code is doing:

extern mod sqlite;

fn db() {

    let database =
        match sqlite::open("test.db") {
            Ok(db) => db,
            Err(e) => {
                println(fmt!("Error opening test.db: %?", e));
                return;
            }
        };

I do understand basically what it is doing. It is attempting to obtain a database connection and also testing for an error. I don't understand exactly how it is doing that.

In order to better understand it, I wanted to rewrite it without the match statement, but I don't have the knowledge to do that. Is that possible? Does sqlite::open() return two variables, or only one?

How can this example be written differently without the match statement? I'm not saying that is necessary or preferable, however it may help me to learn the language.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Brian Oh
  • 9,604
  • 12
  • 49
  • 68

4 Answers4

8

The outer statement is an assignment that assigns the value of the match expression to database. The match expression depends on the return value of sqlite::open, which probably is of type Result<T, E> (an enum with variants Ok(T) and Err(E)). In case it's Ok, the enum variant has a parameter which the match expression destructures into db and passes back this value (therefore it gets assigned to the variable database). In case it's Err, the enum variant has a parameter with an error object which is printed and the function returns.

Without using a match statement, this could be written like the following (just because you explicitly asked for not using match - most people will considered this bad coding style):

let res = sqlite::open("test.db");
if res.is_err() {
    println!("Error opening test.db: {:?}", res.unwrap_err());
    return;
}
let database = res.unwrap();
Chris Morgan
  • 86,207
  • 24
  • 208
  • 215
Zargony
  • 9,615
  • 3
  • 44
  • 44
  • 3
    This code is how I would have answered the question. From here it's informative to look at the definition of [the Result struct](https://github.com/mozilla/rust/blob/master/src/libstd/result.rs#L33), and then the [unwrap](https://github.com/mozilla/rust/blob/master/src/libstd/result.rs#L108) method. You can see `unwrap` uses `match`, and for one case it causes a `fail!`. This is _why_ this is considered bad style: at runtime there's the potential for a failure. The reason `match` is preferable is that the compiler ensures you handle every case explicitly. (Edits: Fixing SO markup.) – nejucomo Oct 19 '13 at 19:20
  • Thanks for the post. I had to make the following minor alteration to make this work "println(fmt!("Error opening test.db: %?", res));". While I may not code like this normally, it does allow me to better understand what is happening. – Brian Oh Oct 20 '13 at 05:43
  • I don't want to sound pedantic, but `Result` actually is an enum, not a struct. ;) But you're right, `match` is great in Rust. On the one hand it ensures that you don't accidentally forget to handle a case since match constructs must be exhaustive. On the other hand it provides destructuring to easily access inner components of data structures. One usually don't want to avoid but encourage using `match`. – Zargony Oct 20 '13 at 14:49
  • @BrianOh I changed the code snipped to use `println(fmt!( … ))` so it works like before. (`println!` uses `format!` rather than `fmt!` and uses a slightly different format syntax (which afaik will be the default in the next release of Rust)) – Zargony Oct 20 '13 at 15:00
  • 1
    fmt! et al. have now been removed from the language. Thus using the new way is a better idea; as those were in 0.8 also, I've updated the answer for forwards-compatibility. – Chris Morgan Oct 22 '13 at 03:36
6

I'm just learning Rust myself, but this is another way of dealing with this.

if let Ok(database) = sqlite::open("test.db") {
    // Handle success case
} else {
    // Handle error case
}

See the documentation about if let.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Dave Hylands
  • 811
  • 10
  • 9
  • For the record, `if let` wasn't available back in 2013 when this question was asked. [`if let` was added one year later.](https://github.com/rust-lang/rust/pull/17634) That doesn't make your answer wrong in any way, though. :) – Francis Gagné Mar 13 '16 at 07:10
4

This function open returns SqliteResult<Database>; given the definition pub type SqliteResult<T> = Result<T, ResultCode>, that is std::result::Result<Database, ResultCode>.

Result is an enum, and you fundamentally cannot access the variants of an enum without matching: that is, quite literally, the only way. Sure, you may have methods for it abstracting away the matching, but they are necessarily implemented with match.

You can see from the Result documentation that it does have convenience methods like is_err, which is approximately this (it's not precisely this but close enough):

fn is_err(&self) -> bool {
    match *self {
        Ok(_) => false,
        Err(_) => true,
    }
}

and unwrap (again only approximate):

fn unwrap(self) -> T {
    match self {
        Ok(t) => t,
        Err(e) => fail!(),
    }
}

As you see, these are implemented with matching. In this case of yours, using the matching is the best way to write this code.

Chris Morgan
  • 86,207
  • 24
  • 208
  • 215
3

sqlite::open() is returning an Enum. Enums are a little different in rust, each value of an enum can have fields attached to it.
See http://static.rust-lang.org/doc/0.8/tutorial.html#enums

So in this case the SqliteResult enum can either be Ok or Err if it is Ok then it has the reference to the db attached to it, if it is Err then it has the error details.

With a C# or Java background you could consider the SqliteResult as a base class that Ok and Err inherit from, each with their own relevant information. In this scenario the match clause is simply checking the type to see which subtype was returned. I wouldn't get too fixated on this parallel though it is a bad idea to try this hard to match concepts between languages.

Chris Sainty
  • 9,086
  • 1
  • 26
  • 31
  • 1
    [`pub type SqliteResult = Result`](https://github.com/linuxfood/rustsqlite/blob/f07a1c3e0e0ce4bbd1ae237f9599edb0394f49e0/types.rs#L121), meaning the [`SqliteResult` that `open` returns](https://github.com/linuxfood/rustsqlite/blob/f07a1c3e0e0ce4bbd1ae237f9599edb0394f49e0/lib.rs#L72) is `Result` (then see the documentation for [`Result`](http://static.rust-lang.org/doc/master/std/result/enum.Result.html) for its definition and what can be done with it). – Chris Morgan Oct 17 '13 at 09:38