Immutable object design pattern

I'm trying to apply this design pattern. In some languages (such as PHP), we'd use e.g. a withX method which returns a clone of the original object with the requested x property.

Am I doing this right?

use err_derive::Error;
use lazy_static::lazy_static;
use regex::Regex;
use std::fmt;
use std::io::{self, BufRead};
use std::num::ParseFloatError;
use std::process;
use std::str::FromStr;

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum TemperatureUnit {
    Celcius,
    Fahrenheit,
}

impl TemperatureUnit {
    fn symbol(&self) -> &str {
        match *self {
            TemperatureUnit::Celcius => "C",
            TemperatureUnit::Fahrenheit => "F",
        }
    }
}

#[derive(Debug, PartialEq)]
struct Temperature {
    value: f32,
    unit: TemperatureUnit,
}

impl Temperature {
    fn convert(&self, unit: TemperatureUnit) -> Self {
        if unit == self.unit {
            return Self { ..*self };
        }

        const CELCIUS_TO_FAHRENHEIT_RATIO: f32 = 1.8;
        const CELCIUS_TO_FAHRENHEIT_OFFSET: u8 = 32;

        let value = match (&self.unit, &unit) {
            (TemperatureUnit::Celcius, TemperatureUnit::Fahrenheit) => {
                self.value * CELCIUS_TO_FAHRENHEIT_RATIO + f32::from(CELCIUS_TO_FAHRENHEIT_OFFSET)
            },
            (TemperatureUnit::Fahrenheit, TemperatureUnit::Celcius) => {
                (self.value - f32::from(CELCIUS_TO_FAHRENHEIT_OFFSET)) / CELCIUS_TO_FAHRENHEIT_RATIO
            },
            _ => unreachable!(),
        };

        Self { value, unit }
    }
}

impl FromStr for Temperature {
    type Err = ParseTemperatureError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.is_empty() {
            return Err(ParseTemperatureError::Empty);
        }

        const TEMPERATURE_PATTERN: &str = r"(?i)(?P<value>\d+(?:\.\d+)?)\s?(?P<unit>C|F)";
        lazy_static! {
            static ref TEMPERATURE_REGEX: Regex = Regex::new(TEMPERATURE_PATTERN).unwrap();
        }

        let caps = match TEMPERATURE_REGEX.captures(s) {
            Some(caps) => caps,
            None => {
                return Err(ParseTemperatureError::Invalid);
            },
        };
        let value: f32 = match (&caps["value"]).parse() {
            Ok(v) => v,
            Err(err) => {
                return Err(ParseTemperatureError::Parse(err));
            },
        };
        let unit = match &caps["unit"] {
            "C" | "c" => TemperatureUnit::Celcius,
            "F" | "f" => TemperatureUnit::Fahrenheit,
            _ => unreachable!(),
        };

        Ok(Temperature { value, unit })
    }
}

impl fmt::Display for Temperature {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}{}", self.value, self.unit.symbol())
    }
}

#[derive(Debug, Error)]
enum ParseTemperatureError {
    #[error(display = "cannot parse temperature from empty string")]
    Empty,
    #[error(display = "invalid temperature literal")]
    Invalid,
    #[error(display = "cannot parse temperature value: {:?}", _0)]
    Parse(#[error(source)] ParseFloatError),
}

fn main() {
    const DEFAULT_INPUT: &str = "36.9C";

    println!("Temperature [{}]:", DEFAULT_INPUT);

    let input = match io::stdin().lock().lines().next() {
        Some(Ok(line)) => line,
        Some(Err(err)) => panic!("Failed to read line: {:?}", err),
        None => {
            eprintln!("No input");
            process::exit(1);
        },
    };
    let input = if input.is_empty() { DEFAULT_INPUT } else { input.trim() };
    let input: Temperature = match input.parse() {
        Ok(t) => t,
        Err(err) => {
            eprintln!("Invalid input: {}", err);
            process::exit(1);
        },
    };

    let output = input.convert(match input.unit {
        TemperatureUnit::Celcius => TemperatureUnit::Fahrenheit,
        TemperatureUnit::Fahrenheit => TemperatureUnit::Celcius,
    });

    println!("{} = {}", input, output);
}

Seems fine to me except four small uglinesses in convert(), which I'd rewrite like this:

fn convert(&self, unit: TemperatureUnit) -> Self {
    const CELCIUS_TO_FAHRENHEIT_RATIO: f32 = 1.8;
    const CELCIUS_TO_FAHRENHEIT_OFFSET: f32 = 32.0;

    let value = match (self.unit, unit) {
        (TemperatureUnit::Celcius, TemperatureUnit::Fahrenheit) => {
            self.value * CELCIUS_TO_FAHRENHEIT_RATIO + CELCIUS_TO_FAHRENHEIT_OFFSET
        },
        (TemperatureUnit::Fahrenheit, TemperatureUnit::Celcius) => {
            (self.value - CELCIUS_TO_FAHRENHEIT_OFFSET) / CELCIUS_TO_FAHRENHEIT_RATIO
        },
        _ => return *self,
    };

    Self { value, unit }
}
1 Like

return *self doesn't work:

error[E0507]: cannot move out of `*self` which is behind a shared reference
  --> src/main.rs:64:25
   |
64 |             _ => return *self,
   |                         ^^^^^ move occurs because `*self` has type `Temperature`, which does not implement the `Copy` trait

Works fine if I change that line to:

_ => self.value,

BTW, it's Celsius, not Celcius.

2 Likes

Oops! :see_no_evil:

I think this is more correct:

fn convert(&self, unit: TemperatureUnit) -> Self {
    const CELSIUS_TO_FAHRENHEIT_RATIO: f32 = 1.8;
    const CELSIUS_TO_FAHRENHEIT_OFFSET: f32 = 32.0;

    let value = match (self.unit, unit) {
        (x, y) if x == y => self.value,
        (TemperatureUnit::Celsius, TemperatureUnit::Fahrenheit) => {
            self.value * CELSIUS_TO_FAHRENHEIT_RATIO + CELSIUS_TO_FAHRENHEIT_OFFSET
        },
        (TemperatureUnit::Fahrenheit, TemperatureUnit::Celsius) => {
            (self.value - CELSIUS_TO_FAHRENHEIT_OFFSET) / CELSIUS_TO_FAHRENHEIT_RATIO
        },
        _ => unreachable!(),
    };

    Self { value, unit }
}

Considering that we might want to add other units... :laughing:

I missed the fact that you didn't make Temperature derive Copy, which you probably should.

I'm sorry if this introduction makes you discouraged. But in general, the immutable object pattern is less useful compared with the other languages, since the Rust takes different approach to solve problem that this pattern was invented for. But it's not useless.

So what's the problem? Shard mutable state. It's bad not only because it effectively prevents parallelism, but also it makes impossible to track state transitions in large project. It's easy for business logics to grow to have large state consists of mesh of inter-referenced objects and its update logics scattered all around of those objects' methods. And it's virtually impossible to debug it until some weird input blow it up on production.

The functional languages addressed this problem by making all its objects immutable. It works great to reduce complexity on state transition logic, so many languages adopted this approach as a library.

But hey, it's the shared mutable state problem. Mutability itself is not evil unless it's shared. What if we can, with some magic, prevent mutation only if the value is shared via multiple references?

Rust have this magic and it's called borrow checker. In Rust we have two kinds of references - shared reference (&T), and unique(mutable) reference (&mut T). You can have as much shared references to the same value at same time, but while an unique reference is alive, you cannot have any references pointing to the same value. All checks are performed at compile time(with some exceptions) so it doesn't slow down the program.

But don't be sad, immutable object pattern allows different kind of data structure and it still can be useful in Rust. It's called persistent data structure. If only small parts of the large state is updated, it seems wasteful to clone the whole state to update that small parts. It would be nice to share the unchanged parts and creates only small diffs efficiently. And right, this is how persistent data structure handle its data.

It would be too long to describe details of persistent data structure here. But you can check already implemented libs out there. Most collection types of clojure's stdlib are implemented in this way, and the JS has ImmutableJS lib. I don't know what libs are used for other languages but there should be more. In Rust, you can check the im crate.

4 Likes

Hmm... Actually what I want to achieve is to return the same shared reference. Returning a copy when nothing has changed seems counter-intuitive. But I understand if it cannot be done / is not the correct way to do things in Rust.

I would not like to do mutation, as that's still added mental burden. If everything is immutable, it's much easier to reason about things.

I'll definitely give im a try, but it doesn't seem relevant in this exercise...

But you are returning a copy in your original code, too. You're not returning the same reference. The convert() method returns the type Self, not &Self. The expression Self { ..*self } makes a copy just like *self albeit in a more roundabout, harder-to-read way.

Yeah, I'm aware of that. I tried looking for a way but couldn't find it.

Finally settled on this code:

use err_derive::Error;
use lazy_static::lazy_static;
use regex::Regex;
use std::fmt;
use std::io::{self, BufRead};
use std::num::ParseFloatError;
use std::process;
use std::str::FromStr;
use strum::IntoEnumIterator;
use strum_macros::EnumIter;

#[derive(Clone, Copy, Debug, EnumIter, Eq, PartialEq)]
enum TemperatureUnit {
    Celsius,
    Fahrenheit,
    Kelvin,
}

impl TemperatureUnit {
    fn symbol(&self) -> &str {
        match self {
            TemperatureUnit::Celsius => "°C",
            TemperatureUnit::Fahrenheit => "°F",
            TemperatureUnit::Kelvin => "K",
        }
    }

    fn symbol_regex(&self) -> &Regex {
        const CELSIUS_PATTERN: &str = r"°?C";
        const FAHRENHEIT_PATTERN: &str = r"°?F";
        const KELVIN_PATTERN: &str = r"K";

        lazy_static! {
            static ref CELSIUS_REGEX: Regex = Regex::new(CELSIUS_PATTERN).unwrap();
            static ref FAHRENHEIT_REGEX: Regex = Regex::new(FAHRENHEIT_PATTERN).unwrap();
            static ref KELVIN_REGEX: Regex = Regex::new(KELVIN_PATTERN).unwrap();
        }

        match self {
            TemperatureUnit::Celsius => &CELSIUS_REGEX,
            TemperatureUnit::Fahrenheit => &FAHRENHEIT_REGEX,
            TemperatureUnit::Kelvin => &KELVIN_REGEX,
        }
    }
}

impl fmt::Display for TemperatureUnit {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.symbol())
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
struct Temperature {
    value: f32,
    unit: TemperatureUnit,
}

impl Temperature {
    fn convert(&self, unit: TemperatureUnit) -> Result<Self, TemperatureConversionError> {
        const CELSIUS_TO_FAHRENHEIT_RATIO: f32 = 1.8;
        const CELSIUS_TO_FAHRENHEIT_OFFSET: f32 = 32.0;
        const CELSIUS_TO_KELVIN_OFFSET: f32 = 273.15;

        let value = match (self.unit, unit) {
            (ref x, ref y) if x == y => return Ok(*self),
            (TemperatureUnit::Celsius, TemperatureUnit::Fahrenheit) => {
                self.value * CELSIUS_TO_FAHRENHEIT_RATIO + CELSIUS_TO_FAHRENHEIT_OFFSET
            }
            (TemperatureUnit::Celsius, TemperatureUnit::Kelvin) => {
                self.value + CELSIUS_TO_KELVIN_OFFSET
            }
            (TemperatureUnit::Fahrenheit, TemperatureUnit::Celsius) => {
                (self.value - CELSIUS_TO_FAHRENHEIT_OFFSET) / CELSIUS_TO_FAHRENHEIT_RATIO
            }
            (TemperatureUnit::Fahrenheit, TemperatureUnit::Kelvin) => {
                self.convert(TemperatureUnit::Celsius)?
                    .convert(TemperatureUnit::Kelvin)?
                    .value
            }
            (TemperatureUnit::Kelvin, TemperatureUnit::Celsius) => {
                self.value - CELSIUS_TO_KELVIN_OFFSET
            }
            (TemperatureUnit::Kelvin, TemperatureUnit::Fahrenheit) => {
                self.convert(TemperatureUnit::Celsius)?
                    .convert(TemperatureUnit::Fahrenheit)?
                    .value
            }
            _ => {
                return Err(TemperatureConversionError::NotSupported {
                    from: self.unit,
                    to: unit,
                })
            }
        };

        Ok(Self { value, unit })
    }
}

impl FromStr for Temperature {
    type Err = ParseTemperatureError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.is_empty() {
            return Err(ParseTemperatureError::Empty);
        }

        lazy_static! {
            static ref TEMPERATURE_PATTERN: String = format!(
                r"(?i)(?P<value>(?:-|−)?\d+(?:\.\d+)?)\s?(?P<unit>{})",
                TemperatureUnit::iter()
                    .map(|u| u.symbol_regex().to_string())
                    .collect::<Vec<String>>()
                    .join("|")
            );
            static ref TEMPERATURE_REGEX: Regex = Regex::new(&TEMPERATURE_PATTERN).unwrap();
        }

        let caps = match TEMPERATURE_REGEX.captures(s) {
            Some(caps) => caps,
            None => {
                return Err(ParseTemperatureError::Invalid);
            }
        };
        let value = &caps["value"];
        let value: f32 = match value.replace("−", "-").parse() {
            Ok(v) => v,
            Err(err) => {
                return Err(ParseTemperatureError::Parse {
                    source: err,
                    value: String::from(value),
                });
            }
        };
        let unit = &caps["unit"];
        let unit = TemperatureUnit::iter()
            .find(|u| u.symbol_regex().is_match(unit))
            .unwrap();

        Ok(Temperature { value, unit })
    }
}

impl fmt::Display for Temperature {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} {}", self.value, self.unit.symbol())
    }
}

#[derive(Debug, Error)]
enum TemperatureConversionError {
    #[error(display = "conversion from {} to {} is not supported", from, to)]
    NotSupported {
        from: TemperatureUnit,
        to: TemperatureUnit,
    },
}

#[derive(Debug, Error)]
enum ParseTemperatureError {
    #[error(display = "cannot parse temperature from empty string")]
    Empty,
    #[error(display = "invalid temperature literal")]
    Invalid,
    #[error(display = "invalid temperature value: {}", value)]
    Parse {
        #[error(source)]
        source: ParseFloatError,
        value: String,
    },
}

fn main() {
    const DEFAULT_INPUT: &str = "36.9C";

    println!("Temperature [{}]:", DEFAULT_INPUT);

    let input = match io::stdin().lock().lines().next() {
        Some(Ok(line)) => line,
        Some(Err(err)) => panic!("Failed to read line: {:?}", err),
        None => {
            eprintln!("No input");
            process::exit(1);
        }
    };
    let input = if input.is_empty() {
        DEFAULT_INPUT
    } else {
        input.trim()
    };
    let input: Temperature = match input.parse() {
        Ok(t) => t,
        Err(err) => {
            eprintln!("Invalid input: {}", err);
            process::exit(1);
        }
    };

    println!("{}", input);

    let outputs = TemperatureUnit::iter()
        .filter(|u| u != &input.unit)
        .map(|u| input.convert(u))
        .filter(|r| match r {
            Ok(_) => true,
            Err(TemperatureConversionError::NotSupported { from: _, to: _ }) => false,
            // Err(err) => panic!("Temperature conversion failed: {:?}", err),
        })
        .collect::<Result<Vec<Temperature>, TemperatureConversionError>>()
        .expect("Temperature conversion failed");

    for output in outputs {
        println!("= {}", output);
    }
}

In Rust, you can check the im crate.

So I've been using im, but I'm curious as to why there's no "true" (exterior) immutability.

For example, from im::Vector - Rust

let mut vec = vector![1, 2, 3];
vec.push_back(4);
assert_eq!(vector![1, 2, 3, 4], vec);

It still requires mut.

Why not something like this?

let vec = vector![1, 2, 3];
let vec = vec.push_back(4);
assert_eq!(vector![1, 2, 3, 4], vec);

And for im::Vector - Rust

let mut vec = vector![1, 2, 3];
assert_eq!(Some(3), vec.pop_back());
assert_eq!(vector![1, 2], vec);

Why not this?

let vec = vector![1, 2, 3];
let (vec, popped) = vec.pop_back();
assert_eq!(Some(3), popped);
assert_eq!(vector![1, 2], vec);

Like Data.Stack, I guess.

An immutable vector can't be implemented as efficiently as a mutable one and puts more constraints on the element type.

The reason is that it requires structural sharing, which implies a tree or similar linked structure – usually an ordered trie or a hash array mapped trie (cf. Bagwell's 2000 paper), both of which would in turn demand lots of pointer traversal resulting in cache misses, as well as at least an Ord or a Hash bound on the element type.

That kind of stuff just doesn't fly as the default dynamic array type in a systems language. Rust doesn't want to, and doesn't pretend to, be a pure functional language.

As I understand it, im already uses structural sharing (by Arc). So it's just a matter of a "true" immutable API, isn't it?

But perhaps I should open an issue on im instead... Just want to check that my understanding is correct, as I'm still a newbie.

Oh, alright, sorry, are you asking why this specific library doesn't provide such an API? Indeed it looks like it should. I missed that.

1 Like

Probably so that you can use it as a drop-in replacement for Vec or VecDeque without rewriting your whole program.

Note that it's possible to write the "immutable API" in terms of the mutating one:

fn push_front<T: Clone>(v: &Vector<T>, arg: T) -> Vector<T> {
    let mut v = v.clone(); // O(1)
    v.push_front(arg);     // O(1)*
    v
}

fn pop_back<T: Clone>(v: Vector<T>) -> (Option<T>, Vector<T>) {
    let value = v.pop_back(); // O(1)*
    (value, v)
}

However, it's not possible to go the other way around: you can't write pop_back(&mut self) -> Option<T> in terms of the function above (without some major caveats). So the API provided by the im crate is more expressive than the "purely immutable" one. Actually on second thought you could do that if all the self arguments are by reference, like I did for push_front. Not sure why that didn't occur to me. Anyway, IMO there's still value in having an API that is a drop-in replacement for VecDeque.

2 Likes

Rust is closer to hardware and more pragmatic about immutability than languages focused on more abstract concepts. In Rust immutability is not the goal, but only one of many tools to ensure memory safety. In short, Rust doesn't care whether something is mutable or not as long as it can't cause any problems.

Rust enforces ownership and shared/exclusive borrows (&/&mut) with hard guarantees. In contrast, the mut next to bindings is just a cosmetic detail that doesn't even do much, e.g.:

let vec = vec![1,2,3];
// vec.push(4); // not allowed without `let mut`
{vec}.push(4); // yes, works! Not a problem, because it's still safe.

(that "hack" is explained here)

Rust also has a concept of interior mutability that cannot be forbidden, so anything that looks immutable can still mutate itself as long as it does it safely (e.g. through mutex or atomics).

5 Likes

Mind = blown :exploding_head: