Guys, it happened! After
long Six weeks of waiting , the Rust 1.15 version with blackjack and procedural macros finally came out .
In my indiscreet opinion, this is the most significant release, after the epic 1.0 . Among the many tasty things in this release, procedural macros have been stabilized, exploding the brain with their power, convenience and security.
And what does this give to mere mortals? Virtually free [de] serialization , user - friendly interface to the database , an intuitive web framework , output constructors, and much more.
Yes, if you still have not got to this language, now is the time to try, especially since now you can install the compiler and the environment with one command:
curl https://sh.rustup.rs -sSf | sh
However, first things first.
For a long time, only standard types could be automatically displayed, such as Eq , PartialEq , Ord , PartialOrd , Debug , Copy , Clone . Now it is possible for custom types.
Instead of manual implementation, just write #[derive(_)]
, and the compiler will do the rest for us:
#[derive(Eq, Debug)] struct Point { x: i32, y: i32, }
Programmers working with Haskell should have all this very familiar (including the names), and it is used in about the same cases. The compiler, having detected the derive
attribute, traverses a list of types and implements for them a standard set of methods to the best of its understanding.
For example, for type Eq
, the method fn eq(&self, other: &Point) -> bool
will be implemented by sequential comparison of the structure fields. Thus, structures will be considered equal if their fields are equal.
Of course, in cases where the desired behavior differs from the default behavior, the programmer can determine the implementation of the type personally, for example:
use std::fmt; impl fmt::Debug for Point { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "My cool point with x: {} and y: {}", self.x, self.y) } }
Whatever it was, automatic typing noticeably simplifies coding and makes the text of the program more readable and concise.
A compiler, even as smart as Rust, cannot derive all methods on its own. However, in some cases, I want to be able to tell the compiler how to derive certain methods for non-standard structures, and then give him complete freedom of action. Such an approach can be used in quite complex mechanisms, eliminating the need for a programmer to write code with his hands.
Procedural macros allow you to add a metaprogramming element to the language and thus significantly simplify routine operations, such as serialization or request processing.
Well, well, you say, this is all wonderful, but where are the examples?
Often there is the problem of transferring data to another process, sending them over the network or writing to disk. Well if the data structure is simple and can be easily represented as a sequence of bytes.
And if not? If the data is a set of complex structures with rows of arbitrary length, arrays, hash tables and B-trees? One way or another, the data will have to be serialized.
Of course, in the history of Computer Science this problem arose several times and the answer usually lies in the serialization libraries, like Google Protobuf .
Traditionally, a programmer builds a meta description of data and protocols in a special declarative language, and then the whole thing is compiled into code that is already used in business logic.
In this sense, Rust is no exception, and there really is a library for serialization . But there is no need to write any meta descriptions. Everything is implemented by means of the language itself and the mechanism of procedural macros:
// - #[macro_use] extern crate serde_derive; // JSON extern crate serde_json; // #[derive(Serialize, Deserialize, Debug)] struct Point { x: i32, y: i32, } fn main() { // let point = Point { x: 1, y: 2 }; // JSON let serialized = serde_json::to_string(&point).unwrap(); // : serialized = {"x":1,"y":2} println!("serialized = {}", serialized); // JSON Point let deserialized: Point = serde_json::from_str(&serialized).unwrap(); // , : deserialized = Point { x: 1, y: 2 } println!("deserialized = {:?}", deserialized); }
In addition to JSON, the Serde library also supports a lot of formats: URL, XML, Redis, YAML, MessagePack, Pickle, and others. Out of the box, serialization and deserialization of all containers from the standard Rust library is supported.
It looks like magic, only it is not magic. Everything works through a kind of introspection at the compilation stage. This means that all errors will be caught and corrected in a timely manner.
Speaking of deserialization. Above, we saw how you can take a JSON string and get a structure with filled fields from it. The same approach can be applied to reading configuration files.
It is enough to create a file in one of the supported formats and simply deserialize it into the configuration structure instead of dull parsing and parsing the parameters one by one.
Of course, business is not limited to one serialization. For example, the Diesel library provides a convenient interface to databases, which was also made possible by procedural macros and automatic output of methods in Rust:
// ... #[derive(Queryable)] pub struct Post { pub id: i32, pub title: String, pub body: String, pub published: bool, } // ... fn main() { let connection = establish_connection(); let results = posts.filter(published.eq(true)) .limit(5) .load::<Post>(&connection) .expect("Error loading posts"); println!("Displaying {} posts", results.len()); for post in results { println!("{}", post.title); println!("----------\n"); println!("{}", post.body); } }
A full example can be found on the library website.
Maybe we want to process a user request? Again, the language features allow you to write intuitive code that “just works.”
Below is a sample code using the Rocket framework which implements the simplest counter:
struct HitCount(AtomicUsize); #[get("/")] fn index(hit_count: State<HitCount>) -> &'static str { hit_count.0.fetch_add(1, Ordering::Relaxed); "Your visit has been recorded!" } #[get("/count")] fn count(hit_count: State<HitCount>) -> String { hit_count.0.load(Ordering::Relaxed).to_string() } fn main() { rocket::ignite() .mount("/", routes![index, count]) .manage(HitCount(AtomicUsize::new(0))) .launch() }
Or, maybe, it is necessary to process the data from the form?
#[derive(FromForm)] struct Task { complete: bool, description: String, } #[post("/todo", data = "<task>")] fn new(task: Form<Task>) -> String { ... }
In general, it becomes clear that the metaprogramming mechanisms in Rust work very well. And if you remember that the language itself is safe in terms of memory and allows you to write safe multi-threaded code , free from the state of racing, then everything becomes quite good.
I am very pleased that now these features are available in a stable version of the language, because many complained that night assemblies had to be used only because of Serde and Diesel. Now there is no such problem.
In the next article I will talk about how to still write these macros and what else you can do with them.
Source: https://habr.com/ru/post/321564/
All Articles