3

Sometimes there are many ways for a program to phrase a message containing a dynamic value to its users. For instance:

  • "{} minutes remaining."
  • "You need to finish in less than {} minutes."

Not all of the messages contain the value as a mere prefix or suffix. In a dynamic language, this would've seemed the logical task for string formatting.

For media where repetitiveness is undesirable (e.g. Slack channels) there are so many different phrasings that producing each final String to output using something like:

pub fn h(x: usize) -> String {
    rand::sample(rand::thread_rng(), vec![
        format!("{} minutes remain.", x),
        format!("Hurry up; only {} minutes left to finish.", x),
        format!("Haste advisable; time ends in {}.", x),
        /* (insert many more elements here) */
    ], 1).first().unwrap_or(format!("{}", x))
}

Would be:

  • Tedious to author, with respect to typing out format!(/*...*/, x) each time.
  • Wasteful of memory+clock-cycles as every single possibility is fully-generated before one is selected, discarding the others.

Is there any way to avoid these shortcomings?

Were it not for the compile-time evaluation of format strings, a function returning a randomly-selected &'static str (from a static slice) to pass into format!, would have been the preferred solution.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
mmirate
  • 704
  • 6
  • 25
  • 2
    Related: [How can I use a dynamic format string with the format! macro?](http://stackoverflow.com/q/32572486/155423). – Shepmaster Sep 11 '16 at 14:44
  • 2
    Have you considered randomly generating a number and using a `match` to pick a string to format? It would avoid generating all the strings then throwing them away. – Aurora0001 Sep 11 '16 at 14:45

3 Answers3

5

Rust supports defining functions inside functions. We can build a slice of function pointers, have rand::sample pick one of them and then call the selected function.

extern crate rand;

use rand::Rng;

pub fn h(x: usize) -> String {
    fn f0(x: usize) -> String {
        format!("{} minutes remain.", x)
    }

    fn f1(x: usize) -> String {
        format!("Hurry up; only {} minutes left to finish.", x)
    }

    fn f2(x: usize) -> String {
        format!("Haste advisable; time ends in {}.", x)
    }

    let formats: &[fn(usize) -> String] = &[f0, f1, f2];
    (*rand::thread_rng().choose(formats).unwrap())(x)
}

This addresses the "wasteful" aspect of your original solution, but not the "tedious" aspect. We can reduce the amount of repetition by using a macro. Note that macros defined within a function are local to that function too! This macro is taking advantage from Rust's hygienic macros to define multiple functions named f, so that we don't need to supply a name for each function when using the macro.

extern crate rand;

use rand::Rng;

pub fn h(x: usize) -> String {
    macro_rules! messages {
        ($($fmtstr:tt,)*) => {
            &[$({
                fn f(x: usize) -> String {
                    format!($fmtstr, x)
                }
                f
            }),*]
        }
    }

    let formats: &[fn(usize) -> String] = messages!(
        "{} minutes remain.",
        "Hurry up; only {} minutes left to finish.",
        "Haste advisable; time ends in {}.",
    );
    (*rand::thread_rng().choose(formats).unwrap())(x)
}
Francis Gagné
  • 60,274
  • 7
  • 180
  • 155
4

My suggestion would be to use a match to avoid unnecessary computation and keep the code as compact as possible:

use rand::{thread_rng, Rng};

let mut rng = thread_rng();
let code: u8 = rng.gen_range(0, 5);
let time = 5;
let response = match code {
    0 => format!("Running out of time! {} seconds left", time),
    1 => format!("Quick! {} seconds left", time),
    2 => format!("Hurry, there are {} seconds left", time),
    3 => format!("Faster! {} seconds left", time),
    4 => format!("Only {} seconds left", time),
    _ => unreachable!()
};

(Playground link)

Admittedly, it's a little bit ugly to match numbers literally, but it's probably the shortest you can get it.

Aurora0001
  • 13,139
  • 5
  • 50
  • 53
  • 1
    My maintainability concern would be the split of the number of choices in the `match` and the number in the `gen_range`. – Shepmaster Sep 11 '16 at 15:10
  • 1
    Yeah, that's annoying but difficult to avoid. I tried to work around that by setting the upper bound to `max_choices` and replacing the irrefutable pattern with `n if n >= max_choices`, but the compiler isn't able to tell that it's exhaustive ([example of what I mean](http://play.integer32.com/?gist=de8cf938ebc0bb5571bc3b406324627a)). – Aurora0001 Sep 11 '16 at 15:32
3

Creating multiple strings is straight-forward to avoid by using closures (or function pointers):

extern crate rand;

use rand::Rng;

pub fn h(x: usize) -> String {
    let messages: &[&Fn() -> String] = &[
        &|| format!("{} minutes remain.", x),
        &|| format!("Hurry up; only {} minutes left to finish.", x),
        &|| format!("Haste advisable; time ends in {}.", x),
    ];
    let default_message = || format!("{}", x);

    rand::thread_rng().choose(messages).unwrap_or(&&(&default_message as &Fn()->String))()
}

fn main() {
    println!("{}", h(1));
}

Notes:

  1. choose instead of sample for one value.
  2. No need for a Vec; an array should be fine.

That's unlikely to improve the "beautiful" aspect. Macros can remove the drudgery:

extern crate rand;

macro_rules! messages {
    {$default: expr, $($msg: expr,)*} => {
        use rand::Rng;

        let messages: &[&Fn() -> String] = &[
            $(&|| $msg),*
        ];
        let default_message = || $default;

        rand::thread_rng().choose(messages).unwrap_or(&&(&default_message as &Fn() -> String))()
    }
}

pub fn h(x: usize) -> String {
    messages! {
        format!("{}", x),
        format!("{} minutes remain.", x),
        format!("Hurry up; only {} minutes left to finish.", x),
        format!("Haste advisable; time ends in {}.", x),
    }
}

fn main() {
    println!("{}", h(1));
}

Notes:

  1. The macro requires at least one argument; this will be used as the default message.
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366