Lessons in the Underscore
I'm toying around with rewriting my website backend in rust, since I find rust more enjoyable to maintain and develop in.
Things were going very smoothly at first, but I ran into a little snag: A little knowledge gap I had to overcome.
The Setup
I have to query a SQL database, and convert the result into a struct of type PageModel. The actual conversion step is handled by a library called sqlx, but this relies on sqlx knowing this is the type we want.
I start with the following information:
- A variable
path, of typeString - A variable
executor, of typeExecutor<Database = Postgres>
My PageModel struct looks something like this. (There are a bunch of other fields, but they are not relavent to this story.)
#[derive(Clone, sqlx::FromRow)]
struct PageModel {
uuid: String,
path: String,
}
The Problem
With the available information, I'm able to create the following:
let response =
sqlx::query_as(
r#"SELECT * FROM "Page" WHERE path = $1;"#
).bind(path)
.fetch_optional(executor)
.await
;
But a problem arises! The above code does not compile, because rust doesn't know which type to convert to. Rust tries to infer what type response should be from context, but there isn't enough information.
The return type of that fetch_optional(executor) method is Result<Option<T>, E>, where T must a set of rules (outside the bounds of this expanation) and E must be sqlx::Error. (Both T and E are what we call 'generic' types, where we let these names take the place of any other type which follows our rules).
Generic types must be specified from left to right. Since Rust already knows the type of E is sqlx::Error, I do not have specify it. But I do have to tell Rust that I want T to be the PageModel type.
Working Through It
To tell Rust what, the most obvious solution is to specify it as part of a variable.
let response: Result<Option<PageModel>> =
sqlx::query_as(
r#"SELECT * FROM "Page" WHERE path = $1;"#
).bind(path)
.fetch_optional(executor)
.await
;
But for for this setup, this is pretty fragile. If for some reason I instead want to use fetch_one or fetch_all, I have to change the type of response to something like Result<PageModel> or Result<Vec<PageModel>>.
I can do better! The function signature of sqlx::query_as looks like query_as::<DB, O>(...), using two generic types: DB and O.
DBis the type of the database (which in this case needs to bePostgres)Ois the type of object to convert to (which in this case needs to bePageModel)
Right now, Rust is already able to infer from the result what I want DB and O to be. But if I specify O as PageModel, then don't need to tell Rust in advance that I want response to be Result<Option<PageModel>, sqlx::Error>
There's a rule in Rust where generic types must be specified left-to-right, and you can't just skip the earlier ones.
Taking this into account, it looks like this.
let response =
sqlx::query_as::<Postgres, PageModel>(
r#"SELECT * FROM "Page" WHERE path = $1;"#
).bind(path)
.fetch_optional(executor)
.await
;
We're almost there! Rust should already know from that we want a Postgres database, it can be inferred from the argument we pass to fetch_optional. Rust already knows the type of executor is Executor<Database = Postgres>, and one of the rules here is that DB and Database must be the same.
But this is where I got stuck for a while: After all, these generic types must be specified from left-to-right, and you can't just skip a type in the list, right?
This is when I was visited by the humble underscore.
The Underscore
In Rust, an underscore (_) acts a special placeholder name. For example, writing _ = generate_some_text() will allow you to call generate_some_text, telling rust explicitly that you are not using the return value.
You can also use an underscore as a placeholder for a type. let hello: _ = "hello".to_string() is the same as saying let hello = "hello".to_string(); You are explicitly telling Rust that it has to infer the type of hello.
This might seem useless in most cases, but it's just what I need here! Previously I had to specify sqlx::query_as::<Postgres, PageModel>. Using the underscore, I can tell Rust to infer the first generic type from context.
let response =
sqlx::query_as::<_, PageModel>(
r#"SELECT * FROM "Page" WHERE path = $1;"#
).bind(path)
.fetch_optional(executor)
.await
;
Ta-daaa! This code compiles, and I don't have to include redundant types in my implementation!
For more detail on the Underscore, see the documentation page on the topic.
Conclusion
Where does this leave me?
To be honest, this wasn't a problem that I necessarily had to solve to the degree I did. But this process allowed me to become more familiar with the language, letting me build more trust in the compiler.