📜 ⬆️ ⬇️

Transfer of intentions

Rust is an elegant language that is somewhat different from many other popular languages. For example, instead of using classes and inheritance, Rust offers its own type-based type system. However, I believe that many programmers starting their acquaintance with Rust (like me) are not aware of common design patterns.


In this article, I want to discuss the design pattern of a new type (newtype), as well as the types of From and Into , which help in the type conversion.


Let's say you work for a European company that creates great digital thermostats for heaters, ready for use on the Internet of Things. So that the water in the heaters does not freeze (and thus did not damage the heaters), we guarantee in our software that if there is a danger of freezing, we will pour hot water over the radiator. Thus, somewhere in our program there is the following function:


 fn danger_of_freezing(temp: f64) -> bool; 

It takes a certain temperature (obtained from the sensors via Wi-Fi) and controls the flow of water accordingly.


Everything is going fine, the customers are satisfied and not a single heater was hurt in the end. The management decides to move to the US market, and soon our company finds a local partner who connects its sensors with our wonderful thermostat.


This is a catastrophe.


After investigation, it turns out that US sensors transmit temperatures in degrees Fahrenheit, while our software works with degrees Celsius. The program starts heating as soon as the temperature drops below 3 ° Celsius. Alas, 3 ° Fahrenheit below freezing point. However, after updating the program, we manage to cope with the problem and the damage amounts to only a few tens of thousands of dollars. Others are less fortunate .


New types


The problem arose from the fact that we used floating-point numbers, meaning something more. We assigned meaning to these numbers without explicitly pointing to it. In other words, our intention was to work with units of measure, and not with ordinary numbers.
Types to the rescue!


 #[derive(Debug, Clone, Copy)] struct Celsius(f64); #[derive(Debug, Clone, Copy)] struct Fahrenheit(f64); 

Programmers writing Rust call this a design pattern. This is a tuple structure containing a single value. In this example, we created two new types, one for degrees Celsius and Fahrenheit.


Our function has acquired the following form:


 fn danger_of_freezing(temp: Celsius) -> bool; 

Using it with anything other than degrees Celsius leads to errors during compilation. Success!


Transformations


All we have to do is to write conversion functions that will convert one unit of measurement to another.


 impl Celsius { to_fahrenheit(&self) -> Fahrenheit { Fahrenheit(self.0 * 9./5. + 32.) } } impl Fahrenheit { to_celsius(&self) -> Celsius { Celsius((self.0 - 32.) * 5./9.) } } 

And then use them, for example, like this:


 let temp: Fahrenheit = sensor.read_temperature(); let is_freezing = danger_of_freezing(temp.to_celsius()); 

From and Into


Conversions between different types are common in Rust. For example, we can turn &str into a String using to_string , for example:


 // ""   &'static str let s = "".to_string(); 

However, it is also possible to use String::from to create strings like this:


 let s = String::from(""); 

Or even like this:


 let s: String = "".into(); 

Why all these functions, when they, at first glance, do the same thing?


In wild nature


Translator’s note: this title contained an untranslatable play on words. The original name Into the Wild can be translated as "In the wild", but you can "Magnificent Into "


Rust offers types that unify conversions from one type to another. std::convert describes, among others, the types From and Into .


 pub trait From<T> { fn from(T) -> Self; } pub trait Into<T> { fn into(self) -> T; } 

As you can see above, the String implements From<&str> , and &str implements Into<String> . In fact, it is enough to implement one of these types to get both, since we can assume that this is the same thing. More precisely, From implements Into .


So let's do the same for temperatures:


 impl From<Celsius> for Fahrenheit { fn from(c: Celsius) -> Self { Fahrenheit(c.0 * 9./5. + 32.) } } impl From<Fahrenheit> for Celsius { fn from(f: Fahrenheit) -> Self { Celsius((f.0 - 32.) * 5./9. ) } } 

We apply this in our function call:


 let temp: Fahrenheit = sensor.read_temperature(); let is_freezing = danger_of_freezing(temp.into()); //  let is_freezing = danger_of_freezing(Celsius::from(temp)); 

I obey and obey


You can argue that we received not so many advantages from the type of From , compared with the implementation of the conversion functions manually, as we did before. You can even say the opposite, that into is much less obvious than to_celsius .


Let's move the conversion of values ​​inside the function:


 // T -  ,       fn danger_of_freezing<T>(temp: T) -> bool where T: Into<Celsius> { let celsius = Celsius::from(temp); ... } 

This function magically accepts both degrees Celsius and Fahrenheit, while remaining type-safe:


 danger_of_freezing(Celsius(20.0)); danger_of_freezing(Fahrenheit(68.0)); 

We can go even further. It is possible not only to process many convertible types, but also to return values ​​of different types in a similar way.


Suppose we need a function that returns a freezing point. It should return degrees Celsius or Fahrenheit, depending on the context.


 fn freezing_point<T>() -> T where T: From<Celsius> { Celsius(0.0).into() } 

Calling this function is slightly different from other functions, where we know the return type for sure. Here we must request the type that we need.


 //     let temp: Fahrenheit = freezing_point(); 

There is a second, more explicit way to call a function:


 //  ,     let temp = freezing_point::<Celsius>(); 

Boxed values


This technique is not only useful for converting values ​​to each other, but also simplifies the processing of packed values, such as results from databases.


 let name: String = row.get(0); let age: i32 = row.get(1); //  let name = row.get_string(0); let age = row.get_integer(1); 

Conclusion


Python has a wonderful Zen .
His first two lines read:


Beautiful is better than ugly.
Explicit is better than implicit.

Programming is the act of transmitting intent to a computer. And we must clearly indicate what exactly we mean when we write programs. For example, a completely inexpressive boolean value to indicate the sort order will not fully reflect our intention. In Rust, we can simply use an enumeration to get rid of any ambiguity:


 enum SortOrder { Ascending, Descending } 

In the same way, new types help to give meaning to simple values. Celsius(f64) is different from Miles(f64) , although they may have the same internal representation ( f64 ). On the other hand, using From and Into helps us simplify programs and interfaces.


Translator's Note:
Thanks to sumproxy and ozkriff for help with the translation.
If you are interested in Rust and you have questions, join us!


')

Source: https://habr.com/ru/post/326896/


All Articles