2
\$\begingroup\$

I'm new to Rust and am working through the exercises found on the bottom of this page. The function below converts a string into pig latin. The requirements are as follows.

Convert strings to Pig Latin, where the first consonant of each word is moved to the end of the word with an added “ay”, so “first” becomes “irst-fay”. Words that start with a vowel get “hay” added to the end instead (“apple” becomes “apple-hay”).

I would like to ensure that my code is as idiomatic as possible, specifically in the string manipulation area. Behold.

fn pigify(text: &str) -> String {
    let map_func = |word: &str| -> String {
        let mut chars = word.chars();
        let first_char = chars.next().unwrap();
        match first_char {
            'a' | 'e' | 'i' | 'o' | 'u' => String::from(word) + "-hay",
            _ => {
                let mut xform = String::new();
                for ch in chars {
                    xform.push(ch);
                }
                xform.push('-');
                xform.push(first_char);
                xform.push_str("ay");
                xform
            }
        }
    };

    let folder = |current: String, next: String| -> String {
        let mut this_str = String::new();
        this_str.push_str(&current);
        if !current.is_empty() {
            this_str.push(' ');
        }
        this_str.push_str(&next);
        this_str
    };

    text.split_whitespace()
        .map(map_func)
        .fold(String::new(), folder)
}
\$\endgroup\$
4
\$\begingroup\$

You've got a good use of match and some solid iterator usage, very nice!

  1. Your closures don't need to be closures -- they don't capture any environment. You can just make them functions (even defined inside the other function, if you'd like). Normally, prefer to define closures at the site of use, instead of as variables ahead of the use. This allows type inference to work on them more effectively (probably why you had to specify types for the closure arguments and return values).

  2. Chars::as_str allows you to get the remainder of the underlying string, so you don't need to iterate though all the remaining characters.

  3. The format! macro allows you to build a string from components in a nicer way.

  4. The code doesn't handle empty strings (unwrap makes this easy to find); there's no reason to not handle them.

  5. You don't need to create a new String in folder, just mark the incoming String as mutable

fn pigify_one(word: &str) -> String {
    let mut chars = word.chars();

    let first_char = match chars.next() {
        Some(c) => c,
        None => return String::new(),
    };

    match first_char {
        'a' | 'e' | 'i' | 'o' | 'u' => format!("{}-hay", word),
        _ => format!("{}-{}ay", chars.as_str(), first_char),
    }
}

fn folder(mut current: String, next: String) -> String {
    if !current.is_empty() {
        current.push(' ');
    }
    current.push_str(&next);
    current
}

fn pigify(text: &str) -> String {
    text.split_whitespace()
        .map(pigify_one)
        .fold(String::new(), folder)
}

I'm a big fan of itertools, which provides methods like join:

extern crate itertools;

use itertools::Itertools;

fn pigify(text: &str) -> String {
    text.split_whitespace()
        .map(pigify_one)
        .join(" ")
}

See What's an idiomatic way to print an iterator separated by spaces in Rust?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.