Problem overview
A thermostat keeps a target temperature, and the target must always stay between 10 and 30 degrees. If any code could assign to the target directly, nothing would stop it from setting 95. So the target is a private field, and only the thermostat's own methods change it. Each of them keeps the value in range.
You write the Thermostat struct with one private field, target (an i32), and these functions:
Thermostat::new()returns a thermostat whose target is 20.set_target(t)sets the target tot. A value below 10 sets it to 10, and a value above 30 sets it to 30.warmer()raises the target by 1 andcooler()lowers it by 1, but neither ever leaves the range 10 to 30.get_target()returns the target. It is the only way for other code to read it.
For example, a new thermostat reads 20. Two warmer() calls take it to 22, and cooler() brings it back to 21. After set_target(35) it reads 30, and warmer() leaves it at 30.
Approach
The real work is one rule: every new target is clamped into 10 to 30. A value that is too low isn't refused. The target becomes the lowest one allowed, and in the same way a too-high value becomes 30. Values from 10 to 30, endpoints included, are kept as they are.
If warmer and cooler each repeated that rule, there would be three copies to keep in step. Instead they call set_target with self.target + 1 or self.target - 1. At 30, warmer asks for 31 and set_target turns it back into 30. The rule lives in one place.
Privacy is what makes the rule worth anything. In Rust a struct field is private by default: without pub, code in other modules can't read or change it. This lesson writes the struct and its methods in one module, but code in another module would have to go through set_target, warmer, cooler and get_target. With a field marked pub, it could write t.target = 95 and break the rule.
Step by step
- Declare the field as
target: i32, withoutpub. - Start at 20 in
new, with the struct literal as the last expression. - In
set_target, handle the low side: ift < 10, setself.targetto 10. - Handle the high side: an
else if t > 30that sets 30. - Everything else is in range: an
elsethat storest. The values 10 and 30 themselves end up here. warmercallsself.set_target(self.target + 1)andcoolercallsself.set_target(self.target - 1).get_targetreturnsself.target, with no semicolon.
The solution in Rust
struct Thermostat {
target: i32,
}
impl Thermostat {
fn new() -> Thermostat {
Thermostat { target: 20 }
}
fn set_target(&mut self, t: i32) {
if t < 10 {
self.target = 10;
} else if t > 30 {
self.target = 30;
} else {
self.target = t;
}
}
fn warmer(&mut self) {
self.set_target(self.target + 1);
}
fn cooler(&mut self) {
self.set_target(self.target - 1);
}
fn get_target(&self) -> i32 {
self.target
}
}
A run that uses every method:
let mut t = Thermostat::new();
println!("{}", t.get_target());
t.warmer();
t.warmer();
println!("{}", t.get_target());
t.cooler();
println!("{}", t.get_target());
t.set_target(35);
println!("{}", t.get_target());
t.warmer();
println!("{}", t.get_target());
t.set_target(4);
println!("{}", t.get_target());
t.cooler();
println!("{}", t.get_target());
t.set_target(10);
t.warmer();
println!("{}", t.get_target());
It prints, one number per line:
20
22
21
30
30
10
10
11
Rust details worth knowing:
&mut selfand&self. The methods that change the target take&mut self, a mutable borrow.get_targetonly reads, so&selfis enough. The variable that holds the thermostat must be declaredlet mutto call the first group.- Calling one method from another. Inside
warmer,selfis already a mutable borrow, so it can callself.set_target(...)directly. The argumentself.target + 1is computed first, so there is no clash between the read and the write. - The privacy error. Outside the module, an assignment such as
t.target = 95fails to compile with "fieldtargetof structThermostatis private". The compiler enforces the rule, not a convention. - Value in, value out.
set_targetreturns nothing, so every branch ends with a semicolon.get_targetreturns a number, so its body is an expression with no semicolon.
Complexity
Each method does a few comparisons and one assignment, so every call is O(1) time and the struct uses O(1) space.
Edge cases and common mistakes
- The endpoints. 10 and 30 are allowed, so
set_target(10)keeps 10. The checks are strict (t < 10andt > 30), so the endpoints fall through to the last branch and are stored unchanged. - Refusing instead of clamping.
set_target(35)doesn't leave the old target in place. It sets 30. - Stepping past the end. At 30,
warmerleaves the target at 30, and at 10,coolerleaves it at 10. Writingself.target += 1directly would reach 31. - Making the field
pub. It would compile and pass simple examples, but other code could then store 95. - Very large or small values. A
tof -1000 gives 10 and 1000 gives 30. Acooler()call after that gives 29, so the thermostat can always step again from the end it landed on.
Testing
- A new thermostat:
get_target()gives 20. After twowarmer()calls it gives 22, and aftercooler()it gives 21. set_target(35),get_target(),warmer(),get_target(),cooler(),get_target()gives 30, 30, 29.set_target(4),get_target(),cooler(),get_target()gives 10, 10. Thenset_target(10),warmer(),get_target()gives 11.
