Specialized Generic Methods in Rust

I have not been so interested in a language in years as I am right now with Rust. Readers of De Programmatica Ipsum have probably sensed this last Monday when we published “The Great Rewriting in Rust”, which has been a hit article this week.

To say that I’m fascinated by Rust is an understatement.

So, as an exercise, I wanted to replicate in Rust one of the subsystems I used for my master thesis project back in 2008. That one was a C++03 project; back then C++11 was still in the works, so I could not use many new features that are nowadays common in the standard, like std::tuple and others.

That project had a homemade ORM, basically implementing all objects as std::map of properties using the Poco::Any object. Those “properties” were just generic key/value pairs, and I got the inspiration from a C++ book I had read for work, “Financial Instrument Pricing Using C++” by Daniel Duffy (book which has since been updated to C++11). The final goal of that ORM was to save a complete object graph into a SQLite file.

Here is the original code for the implementation of the property pattern I used in my project.

So, how would we do this in Rust? I started with the most naive implementation, and to my surprise (given my rookie level of Rust knowledge) it just worked:

struct Property<T> {
    name: String,
    value: T,
}

I added a convenience function attached to that struct, just to have a nicer way to create new instances in my code:

impl<T> Property<T> {
    fn create(name: &str, value: T) -> Property<T> {
        Property {
            name: String::from(name),
            value: value,
        }
    }
}

Now I needed to create a bag structure holding lots of these things:

struct PropertyMap<T> {
    pub values: HashMap<String, Property<T>>,
}

And of course I added a set of convenience functions to help me use this type:

impl<T> PropertyMap<T> {
    fn create(size: usize, properties: Vec<Property<T>>) -> PropertyMap<T> {
        let mut values: HashMap<String, Property<T>> = HashMap::with_capacity(size as usize);
        for property in properties {
            let name = property.name.clone();
            values.insert(name, property);
        }
        PropertyMap { values: values }
    }

    fn get(&self, key: &str) -> Result<&Property<T>, String> {
        if self.values.contains_key(key) {
            return Ok(&self.values[key]);
        }
        return Err(format!("Value '{}' not found", key));
    }
}

Beautiful implementation of get() with the very handy Result type. Of course, now I need a way to pretty-print these things in the console:

impl<T> fmt::Display for Property<T>
where
    T: fmt::Display,
{
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{} = {}", self.name, self.value)
    }
}

impl<T> fmt::Display for PropertyMap<T>
where
    T: fmt::Display,
{
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        for (key, value) in &(self.values) {
            writeln!(f, "{} => {}", key, value)?;
        }
        return Ok(());
    }
}

So far, so good. Now I can start playing with those things:

fn main() {
    let a = Property::create("age", 22);

    // Create map and print it
    let map: PropertyMap<i32> =
        PropertyMap::create(30, vec![a.clone()]);
    println!("Full map =>");
    println!("{}", map);

    // Try to find item in map
    for key in ["tata", "toti", "age"].iter() {
        let found = map.get(key);
        match found {
            Ok(value) => println!("FOUND '{}' => {}", key, value),
            Err(message) => println!("{}", message),
        }
    }

    // Create properties of other types
    let b = Property::create("name", "Roger");
    let c = Property::create("valid", true);
    let d = Property::create("cost", 6666.25);
}

Looking good! Now I also implemented equality, so that I can compare properties with one another:

impl<T> cmp::Eq for Property<T> where T: cmp::PartialEq {}

impl<T> cmp::PartialEq for Property<T>
where
    T: cmp::PartialEq,
{
    fn eq(&self, other: &Property<T>) -> bool {
        return self.name == other.name && self.value == other.value;
    }
}

fn main() {
    let a = Property::create("age", 22);
    let a1 = a.clone();
    let a2 = Property::create("age", 256);
    let a3 = Property::create("whatever", 22);

    println!("{}", a == a1); // true
    println!("{}", a == a2); // false
    println!("{}", a == a3); // false
}

Of course, it does not make sense to compare properties that are not of the same type; the compiler would complain, anyway.

Now I wanted to push things a bit further; when creating SQL statements for inserting or updating, we need to surround strings with single quotes; my first reaction was to create a generic format_for_database() function, and then provide a specialized version for Property<String> and Property<&'static str>, but I quickly found out that this is currently not possible in Rust.

So I ended up doing the following instead:

type IntProperty = Property<i32>;
type FloatProperty = Property<f64>;
type BoolProperty = Property<bool>;
type StringProperty = Property<&'static str>;

impl IntProperty {
    fn format_for_database(&self) -> String {
        format!("{} = {}", self.name, self.value)
    }
}

impl BoolProperty {
    fn format_for_database(&self) -> String {
        let val = if self.value { "TRUE" } else { "FALSE" };
        format!("{} = {}", self.name, val)
    }
}

impl FloatProperty {
    fn format_for_database(&self) -> String {
        format!("{} = {}", self.name, self.value)
    }
}

impl StringProperty {
    fn format_for_database(&self) -> String {
        format!("{} = '{}'", self.name, self.value)
    }
}

Now we can do very polymorphic calls like this:

fn main() {
    let a = Property::create("age", 22);
    println!("Property a => {}", a.format_for_database());

	let b = Property::create("name", "Roger");
    println!("{}", b.format_for_database());

    let c = Property::create("valid", true);
    println!("{}", c.format_for_database());

    let d = Property::create("cost", 6666.25);
    println!("{}", d.format_for_database());
}

Verbose, but gets things done, waiting until the language supports such feature. Ideally I would have implemented a generic format_for_database() function in the generic impl<T> block, but if I do that, I get error E0592, complaining about a duplicate definition.

You can find the final code in my GitLab account. The next step for this project will be to use Rust’s own Any trait and translate the original C++ code into a working Rust version.