0
\$\begingroup\$

I'm learning Rust by solving exercises from different tracks on Exercism.

The below code is an implementation of "Protein Translation" from the Python track. I split protein into separate functions - nucleotide_sequence and codon_translation, which I think improves readability.

fn nucleotide_sequence(rna: &str) -> Vec<&str> {
    let mut position = 0;
    let mut codons = Vec::<&str>::with_capacity(rna.len());

    if rna.len() % 3 != 0 {
        return codons;
    } else {
        while position < rna.len() {
            let mut sequence_length = 0;
            for codon in rna.chars().by_ref().take(3) {
                sequence_length += codon.len_utf8();
            }
            codons.push(&rna[position..position + sequence_length]);
            position += sequence_length;
        }
        codons
    }
}

fn codon_translation(sequence_chunks: Vec<&str>) -> Vec<&str> {
    let mut polypeptide = Vec::new();
    for codon in sequence_chunks.iter() {
        match *codon {
            "AUG" => polypeptide.push("Methionine"),
            "UUU" | "UUC" => polypeptide.push("Phenylalanine"),
            "UUA" | "UUG" => polypeptide.push("Leucine"),
            "UCU" | "UCC" | "UCA" | "UCG" => polypeptide.push("Serine"),
            "UAU" | "UAC" => polypeptide.push("Tyrosine"),
            "UGU" | "UGC" => polypeptide.push("Cysteine"),
            "UGG" => polypeptide.push("Tryptophan"),
            "UAA" | "UAG" | "UGA" => break,
            _ => break,
        }
    }
    polypeptide
}

fn proteins(rna: &str) -> Vec<&str> {
    let chunks = nucleotide_sequence(rna);
    let sequence = codon_translation(chunks);
    sequence
}

#[test]
fn test_nucleotide_sequence() {
    let data = [
        (
            "AUG",
            vec!["AUG"],
            "RNA has one codon (nucleotide sequence)",
        ),
        (
            "AUGUUUUCU",
            vec!["AUG", "UUU", "UCU"],
            "RNA has three codons",
        ),
        (
            "XY",
            vec![],
            "RNA cannot be broken into 3 nucleotide sequences",
        ),
        ("", vec![], "RNA doesn't exist; no codons can be formed ..."),
    ];

    for (rna, codon_chunks, description) in data {
        assert_eq!(nucleotide_sequence(rna), codon_chunks, "{}", description)
    }
}

#[test]
fn test_codon_translation() {
    let data = [
        (
            vec!["UUU", "UUU"],
            vec!["Phenylalanine", "Phenylalanine"],
            "Codon 'UUUUUU' translates to RNA sequence ['Phenylalanine', 'Phenylalanine']"
        ),
        (
            vec!["AUG", "UUU", "UGG"],
            vec!["Methionine", "Phenylalanine", "Tryptophan"],
            "Codon 'AUGUUUUGG' translates to RNA sequence ['Methionine', 'Phenylalanine', 'Tryptophan']"
        ),
        (
            vec!["UAG", "UGG"], vec![], "First codon corresponds to STOP, so others are not taken into consideration"
        ),
        (
            vec!["UGG", "UAG"],
            vec!["Tryptophan"],
            "Codon 'UGGUAG' correponds to only 'Tryptophan'"),
        (
            vec!["UGG", "UGU", "UAU", "UAA", "UGG", "UUU"],
            vec!["Tryptophan", "Cysteine", "Tyrosine"],
            "Codon corresponding to STOP is in the middle, whihch makes the RNA sequence '['Tryptophan', 'Cysteine', 'Tyrosine']"
        ),
        (
            vec!["ABC", "XYZ"],
            vec![],
            "Invalid codon, no matching polypeptides found ...",
        ),
    ];

    for (codon_chunks, protein, description) in data {
        assert_eq!(codon_translation(codon_chunks), protein, "{}", description);
    }
}

#[test]
fn test_proteins() {
    let data = [
        (
            "AUGUUUUCU",
            vec!["Methionine", "Phenylalanine", "Serine"],
            "RNA 'AUGUUUUCU' translates to proteins 'Methionine', 'Phenylalanine', 'Serine'",
        ),
        (
            "AUGUUUUCUUAAAUG",
            vec!["Methionine", "Phenylalanine", "Serine"],
            "RNA 'AUGUUUUCUUAAAUG' translates to proteins 'Methionine', 'Phenylalanine', 'Serine'",
        ),
    ];

    for (codons, polypeptides, description) in data {
        assert_eq!(proteins(codons), polypeptides, "{}", description);
    }
}

I'm executing this program as a test binary:

rustc --test protein_translation.rs --verbose -o protein_translation_test.exe
.\protein_translation_test.exe

My approach while learning Rust has been to practice test-driven development, write idiomatic code, and (re)learn common programming concepts correctly.

Is there a better way of breaking down the functionality of protein, solving this problem, or writing tests?

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Good call on splitting that function, it helps with readability.

At a higher level, it appears you are not yet familiar with iterators, and this problem would benefit a lot from some iterator goodness.

Why is the codon translation in a loop? It would be more natural to extract the actual translation to its own function.

I'd also advise creating a proper enum to describe the codon (with their own string translation); but for now I'll just adjust the lifetime of the result to be 'static:

fn translate_codon(codon: &str) -> Option<&'static str> {
    match codon {
        "AUG" => Some("Methionine"),
        "UUU" | "UUC" => Some("Phenylalanine"),
        "UUA" | "UUG" => Some("Leucine"),
        "UCU" | "UCC" | "UCA" | "UCG" => Some("Serine"),
        "UAU" | "UAC" => Some("Tyrosine"),
        "UGU" | "UGC" => Some("Cysteine"),
        "UGG" => Some("Tryptophan"),
        _ => None,
    }
}

Then, we can alter nucleotide_sequence to prepare an iterator of sequences, instead of a vector:

fn sequence_nucleotides(rna: &str) -> impl Iterator<Item = &str> {
    rna.as_bytes()
        .chunks_exact(3)
        .map(|bytes| std::str::from_utf8(bytes).expect("Valid UTF-8"))
}

Note: impl Iterator<...> in return position means that this function returns "something" which implements the Iterator trait, without specifying its exact type.

And finally, we can pull it together in:

fn proteins(rna: &str) -> impl Iterator<Item = &str> {
    sequence_nucleotide(rna)
        .map_while(|codon| translate_codon(nucleotide))
}

And that's it. Simple, ain't it?


The key thing here is to postpone materialization for as long as possible. Iterators are very flexible, with plenty of built-in functionality, and a number of extension crates for more, which is a good reason in itself, but avoiding materialization also means avoiding memory allocations, which is another very good reason.

The second thing is that it is best to split-up "logic" functions (which do the computations) and "orchestrator" functions (which tie things together), in general. Notably to test them separately.

It helps that Iterators provide a lot of "orchestrator" functionality -- such as map_while, which maps until the first None then cuts things short -- and thus using them you can focus on "logic".

This also helps readers because the orchestration functions provided by Iterators are widely known, and therefore users don't have to decrypt the intent of your custom-built orchestration, remaining in chartered waters so to speak, and can focus on the logic.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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