Just now realizing how vm languages make working with strings so easy

I just completed the bottles of beer exercise on Exercism.io. We really have it good with Strings in Java and C# let me tell you. Having to go back and forth between String and &str took me a little while to understand why that was even required. I think this is a place where the Rust book and Rust by Example could use some beefing up.

1 Like

Not just VM languages, but even C++ strings feel a lot easier than rust strings, in part simply because they had no proper equivalent to &str until C++17. (well, okay, there was char *, but meh)

The same relationship also exists between Vec<T> and &[T], though it perhaps doesn't hurt as much since it's not too common that one wants to do something to a list that might change its length (whereas this is frequent with strings).


By the way, if you post your code, maybe we can point out some tips. (also, it's good for the community and those who work on documentation to see what kind of code is written by those exploring the language)

1 Like

Here is my final revision to the Beer song exercise.

pub fn verse(n: i32) -> String {
    let mut verse = String::new();
    if n > 1 {
        verse.push_str(format!("{} of beer on the wall, {} of beer.\nTake one down and pass it around, {} of beer on the wall.\n",
                bottles_string(n), bottles_string(n), bottles_string(n - 1)
        ).as_str());
    } else if n == 1 {
        verse.push_str("1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n");
    } else {
        verse.push_str("No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n");
    }

    return verse;
}

pub fn sing(start: i32, end: i32) -> String {
    let mut song: String = String::new();

    println!("start is: {}, end is: {}", start, end);

    let mut index = start;
    while index >= end {
        song.push_str(verse(index).as_str());
        song.push_str("\n");
        index = index - 1;
    }

    song.pop();
    song
}

fn bottles_string(n: i32) -> String {
    if n > 1 {
        return n.to_string() + " bottles";
    } else {
        return n.to_string() + " bottle";
    }
}

This isn't how I started though. Here was my first attempt:

pub fn verse(n: i32) -> String {
    let mut verse = String::new();
    if n > 1 {
        verse.push_str(bottles_string(n).as_str());
        verse.push_str(" of beer on the wall, ");
        verse.push_str(bottles_string(n).as_str());
        verse.push_str(" of beer.\nTake one down and pass it around, ");
        verse.push_str(bottles_string(n - 1).as_str());
        verse.push_str(" of beer on the wall.\n");
    } else if n == 1 {
        verse.push_str("1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n");
    } else {
       verse.push_str("No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n");
    }

    return verse;
}

pub fn sing(start: i32, end: i32) -> String {

    let mut song: String = String::new();

    println!("start is: {}, end is: {}", start, end);

    let mut index = start;
    while index >= end {
        song.push_str(verse(index).as_str());
        index = index - 1;
    }

    song
}

fn bottles_string(n: i32) -> String {
    if n > 1 {
        return n.to_string() + " bottles";
    } else {
        return n.to_string() + " bottle";
    }
}

I think in Rust it's more idiomatic to create a formatter instead of manually creating a string. This way you're not allocating a String unless needed. For example, if you're printing out your verse immediately, there's no need to store the string in memory.

I'd probably start with something like this:

use std::fmt;
struct Bottles(usize);

impl fmt::Display for Bottles {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self.0 {
            0 if f.alternate() => "No more".fmt(f),
            0 => "no more".fmt(f),
            more => more.fmt(f),
        }?;
        write!(f, " {}", if self.0 == 1 { "bottle" } else { "bottles" })
    }
}

Also you can use &string instead of string.as_str()

In this case it was required to return a String for the tests.

You can easily turn a Display formatter into a string using .to_string()

Notes on random observations:

  • To convert str to String, you can use String::from(s) or s.to_string().
    • s.to_owned() is another, archaic way to do this and the only reason people used to use it was that s.to_string() used to be slow.
    • If the string is a literal, format!("the string") is another option. I'm not sure if this has a performance penalty associated with it, but I often like to use it for consistency with nearby code.
  • string += str; is another way to write string.push_str(str);
  • It is idiomatic to write &string instead of string.as_str() when possible[1]. Basically, this works because &String is allowed to coerce into &str. (for similar reasons, all methods defined on &str and &mut str are available on String).
  • It is idiomatic to not write return on the final statement of a function; just write the thing you are returning, without a semicolon.

Personally, I seldom use addition on strings since format! is quite versatile (as long as you aren't repeatedly using format!() to build onto the same string, which could turn O(n) work into O(n^2)). Here's how I might have written it:

pub fn verse(n: i32) -> String {
    // Checks an implicit contract of the function.
    // (perhaps n should be u32)
    assert!(n >= 0);

    // Note: This one is actually a `&'static str`, which means it is
    //  a reference to an embedded compile time string.  That is possible
    //  in this case because these strings are all constants.
    let take_one_down = match n {
        0 => "Go to the store and buy some more",
        1 => "Take it down and pass it around",
        _ => "Take one down and pass it around",
    };

    let next_n = match n {
        0 => 99,
        n => n - 1,
    };

    format!("{} on the wall, {}.\n{}, {} on the wall.\n",
        bottles_string(n, Case::Upper),
        bottles_string(n, Case::Lower),
        take_one_down,
        bottles_string(next_n, Case::Lower))
}

pub fn sing(start: i32, end: i32) -> String {
    // a more iteratory way to handle the final newline.
    // ('join' is a method on `&[String]` as well as `&[&str]`,
    //  and all &[T] methods are available on `Vec<T>`.)
    let verses: Vec<_> = (start..end + 1).rev().map(verse).collect();
    verses.join("\n")
}

enum Case { Upper, Lower }
fn bottles_string(n: i32, case: Case) -> String {
    match (n, case) {
        (0, Case::Lower) => format!("no more bottles of beer"),
        (0, Case::Upper) => format!("No more bottles of beer"),
        (1, _) => format!("1 bottle of beer"),
        (n, _) => format!("{} bottles of beer", n),
    }
}

pub fn main() {
    println!("{}", sing(0, 99));
}

[1]: &string might not work with some generic functions, as it will produce &String instead of &str when passed as some argument whose type is not explicitly &str.

1 Like

You could easily have written this like so:

 verse += &bottles_string(n) + " of beer on the wall, "
    + &bottles_string(n) + "of beer.\nTake one down and pass it around, "
    + &bottles_string(n) + "of beer on the wall.\n";

I did at first, but thought it was pretty ugly. Turns out both ways are ugly.

While there's far more to it than just this, there's a decent parallel between "If I'm making a new one, I'll use String (Rust) or StringBuilder (C#), but if I'm just accepting an existing one I'll use str (Rust) or string (C#)". (Lifetimes being the huge difference that statement ignores.)

Note that there are crates for simpler string dealing in cases where performance is less important, like easy_strings::EZString - Rust

2 Likes

If you'd like to compare to mine from a while back: http://exercism.io/submissions/c9e17e57220345c28b17c5493713330a

2 Likes

@steveklabnik ,

There are two things in yours that I don't yet understand.

The First:

number=number // The last input to your last format macro

The Second:

<Vec<_>>

number=number // The last input to your last format macro

is a keyword argument to replace the {number} in the format string

.collect::<Vec<_>>()

is saying to collect the previous itterator into a Vec of type _ (an inferred type, basically requesting the type T that was passed into it, in this case the Strings that come from verse)

1 Like

That's very interesting. My solutions looks very overengineered then http://exercism.io/submissions/6091fff9ab1645588db0f1ffcea083d7

You could do this without allocating a vector:

use itertools::Itertools;

pub fn sing(start: i32, end: i32) -> String {
    (start..end + 1).rev().map(verse).join("\n")
}
2 Likes

Can also do it using fold rather than bringing in itertools, although it won't look as pretty :slight_smile:.

I'd be willing to bet the itertools solution is ultimately sloppier with allocations. After all, I can only assume that the reason that the stdlib join takes a slice in the first place is so that it can precompute the total length of the string.

(that said, I would kill to have Itertools::join in the standard library, just because it's so convenient!)


Update: wtf

The stdlib does precompute the total length of the string... incorrectly.

I think.

aw geeze, the str impl is over here, never mind, it's fine guys

@jeramyRR Thanks for sharing! Always fun to see what others make of it!
Here's my over-engineered one :slight_smile:

@StefanoChiodino: I like your Pluralise approach, although I would have returned just self in the singular branch, not format!("{}", self). May save a little allocation overhead?

@steveklabnik: I love the brevity of yours :smiley: It's what I started out with as well, before I decided to follow D.R.Y. into absurdity :blush:

1 Like

Haha makes sense! Must have been mindless copy-pasting!

The brevity of @steveklabnik made me seriously reflect.

2 Likes

That's the great thing about these exercism assignments. They are SO trivial that you have mental capacity to spare to really reflect on your bad habits and style choices. This "going back to basics" has really helped me confront myself.

Exactly! These small exercises really allow you to play with the different trade-offs. In this case, readability Vs brevity Vs Don't-Repeat-Yourself.
Arguably, DRY usually improves readability, but as Steve Vs ours shows, not always :smile:

I believe the appropriate term here is "gold plating": spending lots of time on fancy tricks that add zero functionality :wink: