
no time, jump straight to the enhanced prompt
The emergence of “flow coding” approach presents both opportunities and risks for security-conscious developers. This analysis examines a multi-persona AI prompt technique applied to Rust code review, evaluating its effectiveness and security implications through practical implementation.
The Flow Coding Experiment
Flow coding utilizes AI to simulate expert discussions, potentially surfacing insights that single-perspective reviews might miss. I tested this approach on a Rust protein translation exercise, using a prompt that simulates a panel discussion between programming language experts including Philip Wadler, Edwin Brady, Rich Hickey, and others.
The prompt positions AI as a moderator facilitating discussion between experts with distinct perspectives:
- Type Safety Advocates (Brady, Wadler): Focus on compile-time guarantees
- Pragmatists (Hickey, Beck): Emphasize simplicity and testability
- Rust Specialists (Matsakis, Jung): Address language-specific idioms
- API Design Experts (Snoyman, Boats): Consider developer experience
The key Gemini CLI response to the prompt above was:
....
Philip Wadler: The signature pub fn translate(rna: &str) -> Option<Vec<&str>> is a fine starting point. It correctly identifies the operation as a partial function—it might not produce
a result for all inputs. Option is the natural way in a functional paradigm to represent this possibility of failure. It's simple, and it's honest. The type says "I might give you a
list of proteins, or I might give you nothing."
Edwin Brady: I find it too simple. It's honest about failure, but dangerously vague. The input &str makes almost no guarantees. We know from the problem domain that RNA is not just
any string. It's composed of a specific alphabet. And the output &str for a protein is similarly untyped. We are passing strings around when we should be passing data that represents
our domain. This opens the door to bugs that the type-checker could have caught. We should be encoding our domain invariants in the types themselves.
Rich Hickey: I have to disagree, Edwin. What's the bug? The code checks the inputs. If the string contains an invalid codon, it returns None. If the sequence is incomplete, it returns
None. The contract is fulfilled. Introducing Codon and AminoAcid enums, as I'm sure you're about to suggest, adds a layer of indirection and complexity. The developer has to learn your
types, construct them, and then pass them to the function. Here, they just pass a string. It's direct. It's simple. It solves the problem efficiently.
Niko Matsakis: I think there's a middle path. Rich is right that the current code is simple, but Edwin's point about safety is valid. My main issue isn't the use of &str itself, but
how the failure is communicated. The function can fail for at least three reasons: the input isn't valid UTF-8, a codon is unknown, or the sequence length is wrong. All of these
collapse into a single None.
1 // src/lib.rs:8
2 Err(_) => return None, // Reason 1: Not UTF-8
3
4 // src/lib.rs:12
5 if codon.len() != 3 { // Reason 2: Incomplete
6 return None;
7 }
8
9 // src/lib.rs:23
10 _ => return None, // Reason 3: Unknown Codon
This is un-idiomatic for Rust libraries that expect to be used in larger applications. The caller has no way to distinguish a malformed sequence from, say, a transient UTF-8 error.
The idiomatic solution is Result<Vec<&str>, TranslationError>, where TranslationError is an enum detailing the cause of failure.
Without Boats: Exactly, Niko. API ergonomics are key here. A developer using this function will be frustrated by None. They'll ask, "Why did it fail?" and the API will just shrug.
Providing a specific error type is the most important improvement this code needs. It's the difference between a script and a library component.
Bartosz Milewski: Let's talk about the implementation's structure. It's a very imperative, stateful loop.
1 // src/lib.rs:2
2 let mut proteins = Vec::new();
3 // ...
4 for codon_bytes in codons {
5 // ...
6 proteins.push("...");
7 }
8 // ...
9 Some(proteins)
This pattern of initializing a mutable collection and pushing into it inside a loop is something we try to move away from in functional programming. The logic is centralized and
monolithic. A more functional approach would use iterators to create a processing pipeline. This separates the concerns of chunking the string, translating each chunk, and handling
termination.
Simon Peyton Jones: I couldn't agree more. An iterator-based solution would be far more declarative. You'd describe what you want to happen, not how to do it step-by-step. Something
like: chunk the string into 3-byte slices, map each to a Result containing a protein or an error, take elements until you see a STOP codon, and then collect the results. Each of those
steps is a small, reusable, and easily testable piece of the puzzle. Rust's iterator implementation is brilliant; it will compile down to code that is just as efficient as the
hand-rolled loop, so there's no performance penalty for the higher-level abstraction.
Kent Beck: I'm looking at this code without the tests, and my first thought is, "How would I test this?" The None return makes it difficult. If I pass in "AUGX" and get None, I can't
be sure if it failed because of the X or because the length is 4. If the function returned a TranslationError::InvalidCodon('X') or TranslationError::IncompleteSequence, my tests
could be much more precise. They could assert that specific bad inputs produce specific errors. Good APIs are easy to test, and this API makes testing the negative paths harder than
it needs to be.
Michael Snoyman: Kent's point is crucial for the developer experience. Summing up, the ergonomics here could be vastly improved. The iterator approach that Bartosz and Simon are
advocating for is the clear winner. It's more readable, more composable, and more idiomatic Rust. Combining that with the Result type that Niko and Boats are pushing for would
transform this function.
Rich Hickey: I'll concede that the monolithic None is a problem. Distinguishing errors is a valid concern. I still believe the &str is fine for the domain, but the control flow could
be cleaner. The iterator approach is better; it decomposes the problem. But let's not get carried away with layers of abstraction. The goal is a clear, understandable transformation,
and the iterator pipeline achieves that without needing a complex type system on top.
Moderator: This has been a very focused and productive discussion. Let's distill this into a final set of recommendations for src/lib.rs.
---
Summary of Recommendations for src/lib.rs
1. Replace `Option` with `Result`: This is the highest-priority change. The single None return value hides multiple distinct error conditions. Introduce a TranslationError enum to
provide specific, actionable feedback to the caller (e.g., InvalidCodon, IncompleteSequence).
2. Refactor to an Iterator Pipeline: The current imperative for loop should be replaced with a functional-style iterator chain. This would involve using methods like as_bytes, chunks,
map, take_while, and collect. This change would make the code more declarative, improve readability, and align with Rust's idiomatic "zero-cost abstraction" philosophy.
3. Separate Concerns: Within the refactoring, the logic for mapping a single codon string to a protein string should be extracted into a small, pure helper function (e.g.,
codon_to_protein(...) -> Result<&str, TranslationError>). The main translate function would then call this helper within its iterator chain.
4. Improve Early Exit Logic: The current implementation has multiple return statements. The iterator approach, especially with take_while, provides a more elegant way to handle the
"STOP" codon condition without explicit early returns from the middle of a loop.
5. Retain String Slices (For Now): While a full type-safe representation (AminoAcid enum) has theoretical benefits, the panel agrees that for the scope of this problem, using &str is a
reasonable and simple choice. The primary focus should be on improving the error handling and implementation style.
By implementing these changes, the function would become significantly more robust, ergonomic, and idiomatic without sacrificing the simplicity of its core data representation.
Implementation Results
1. Improved Error Handling `Result` instead of `Option`
Original Issue: Function returned Option<Vec<&str>>
, hiding failure reasons
AI Recommendation: Replace with Result<Vec<&str>, TranslationError>
Prompt
Replace `Option` with `Result`: This is the highest-priority change. The single None return value hides multiple distinct error conditions. Introduce a TranslationError enum to
provide specific, actionable feedback to the caller (e.g., InvalidCodon, IncompleteSequence).
I received the following change (38e2e68ae26cb9df33269dbbe57332953e4d3232) :
#[derive(Debug, PartialEq)]
pub enum TranslationError {
InvalidCodon,
IncompleteSequence,
}
fn protein_from_codon(codon: &str) -> Result<Option<&str>, TranslationError> {
match codon {
"AUG" => Ok(Some("Methionine")),
"UUU" | "UUC" => Ok(Some("Phenylalanine")),
"UUA" | "UUG" => Ok(Some("Leucine")),
"UCU" | "UCC" | "UCA" | "UCG" => Ok(Some("Serine")),
"UAU" | "UAC" => Ok(Some("Tyrosine")),
"UGU" | "UGC" => Ok(Some("Cysteine")),
"UGG" => Ok(Some("Tryptophan")),
"UAA" | "UAG" | "UGA" => Ok(None), // STOP codon
_ => Err(TranslationError::InvalidCodon),
}
}
pub fn translate(rna: &str) -> Result<Vec<&str>, TranslationError> {
rna.as_bytes()
.chunks(3)
.map(|codon_bytes| {
if codon_bytes.len() != 3 {
return Err(TranslationError::IncompleteSequence);
}
std::str::from_utf8(codon_bytes)
.map_err(|_| TranslationError::InvalidCodon)
.and_then(protein_from_codon)
})
.take_while(|res| res.as_ref().map(|opt| opt.is_some()).unwrap_or(true))
.filter_map(|res| res.transpose())
.collect()
}
Security Impact: Explicit error types improve debugging and reduce the likelihood of silent failures that could mask security issues.
Making the return value a Result
implemented also the expected next request about a functional implementation approach of the translate function.
2. Functional Pipeline Adoption
Original Issue: Imperative loop with mutable state
AI Recommendation: Iterator-based functional pipeline
Prompt
Refactor to an Iterator Pipeline: The current imperative for loop should be replaced with a functional-style iterator chain. This would involve using methods like as_bytes, chunks,
map, take_while, and collect. This change would make the code more declarative, improve readability, and align with Rust's idiomatic "zero-cost abstraction" philosophy.
No change applied because applying the first implementation included the resolution of this issue as well.

3. Separation of Concerns – Helper Function Introduction: codon to protein
AI Recommendation: Extract codon translation logic into pure functions
Result: protein_from_codon()
helper function with clear error semantics
Making the return value a Result
created also the expected helper function introduction: named protein_from_codon
instead of codon_to_protein
, however that’s nit picking.
Prompt:
Separate Concerns: Within the refactoring, the logic for mapping a single codon string to a protein string should be extracted into a small, pure helper function (e.g.,
codon_to_protein(...) -> Result<&str, TranslationError>). The main translate function would then call this helper within its iterator chain.
Result:

The extraction into a help function has been applied with the first implementation already; it included the resolution of this issue as well.
4. Early Exit logic
Another recommendation is about an early exit logic checking for the appropriate length.
Prompt
Improve Early Exit Logic: The current implementation has multiple return statements. The iterator approach, especially with take_while, provides a more elegant way to handle the
"STOP" codon condition without explicit early returns from the middle of a loop.
This resulted a whole new verification block (e9a4ee0ea19bece0d4a41ca314d5bb57e8ce1f2d):
if rna.len() % 3 != 0 {
// Handle cases where the RNA string length is not a multiple of 3 but a stop codon is present
let stop_pos = rna.find("UAA").or_else(|| rna.find("UAG")).or_else(|| rna.find("UGA"));
if let Some(pos) = stop_pos {
if pos % 3 != 0 {
return Err(TranslationError::InvalidCodon);
}
let rna_substr = &rna[..pos];
if rna_substr.len() % 3 != 0 {
return Err(TranslationError::IncompleteSequence);
}
} else {
return Err(TranslationError::IncompleteSequence);
}
}

Enums vs. String slices
The prompt below balances pros and cons of enums
and string slices
:
Retain String Slices (For Now): While a full type-safe representation (AminoAcid enum) has theoretical benefits, the panel agrees that for the scope of this problem, using &str is a reasonable and simple choice. The primary focus should be on improving the error handling and implementation style.
Gemini CLI did no change the code as a result:

Reflections on AI suggested changes
1. Formatting and Tooling Integration
The AI-generated code required manual formatting fixes:
My default cargo verification command helped me to fixed that:
cargo test --manifest-path Cargo.toml && cargo fmt --all -- --check
What does 'cargo fmt --all -- --check'
do?
cargo fmt
– Runs the Rust code formatter (rustfmt)--all
– Applies to all packages in the workspace, not just the current one--
– Separates cargo arguments from rustfmt arguments--check
– Runs in check mode, which means it will:- Not modify any files
- Exit with code 0 if formatting is correct
- Exit with code 1 if any files need formatting
- Show which files would be changed
This highlighted the need for integrated tooling in AI-assisted development workflows.
As a result, I applied the proposed changes (efe9323d112db3fee90f0448b0b2a4ccc55978ab):

- let stop_pos = rna.find("UAA").or_else(|| rna.find("UAG")).or_else(|| rna.find("UGA"));
+ let stop_pos = rna
+ .find("UAA")
+ .or_else(|| rna.find("UAG"))
+ .or_else(|| rna.find("UGA"));
if let Some(pos) = stop_pos {
if pos % 3 != 0 {
return Err(TranslationError::InvalidCodon);
Diff in /home/ls/Documents/_dev/exercism/rust/protein-translation/src/lib.rs:28:
}
let rna_substr = &rna[..pos];
if rna_substr.len() % 3 != 0 {
- return Err(TranslationError::IncompleteSequence);
+ return Err(TranslationError::IncompleteSequence);
2. Test Compatibility Issues
Changing Option
with Result
broke existing test expectations, requiring either:
- Test updates to match new API
- Reversion to original implementation
- Wrapper functions for backward compatibility (testing Ok as Result instead of Some as Option)

fix the tests
I “fixed” the tests going back in git history (my favourite tool is tig):


Checking out 8e46e88e9e4a0a7c422cb1aa096610c690cf14cb
and submitting this code to exercism “fixed” the tests.

Enhanced And Security-Focused Prompt
Based on this analysis, I recommend augmenting flow coding prompts with explicit security requirements and additional focus in rust language usage:
- Ensure robust security by strictly validating dependencies
- Sanitize all data inputs and enforce secure communication
- Follow best practices to prevent injection attacks and data leaks
- Beware of unsafe key word usage
Prompt
Please conduct a comprehensive code review of this Rust workspace as if you were moderating a discussion between these experts: Edwin Brady, Philip Wadler, Conor McBride, Simon Peyton Jones, Bartosz Milewski, Ward Cunningham, Kent Beck, Niko Matsakis, Yaron Minsky, Gabriele Keller, Michael Snoyman, Yoshua Wuyts, Rich Hickey, and Ralf Jung.
The workspace aims to provide a functional programming library with extreme type safety while maintaining friendly ergonomics for application developers.
Structure the review as a realistic conversation where:
1. Start by having 2-3 reviewers examine the overall architecture and workspace structure
2. Different experts chime in based on their specialties as they notice things
3. Include natural interruptions, agreements, and disagreements
4. Have them reference specific code examples (with file paths and line numbers where relevant)
5. Show their different perspectives clashing productively (e.g., Hickey questioning complexity vs. Brady defending type safety vs. Jung focusing on solid use Rust, in particular their unsafe parts)
6. Include both praise for clever solutions and criticism of problematic patterns
7. Have them suggest specific, actionable improvements
8. End with a summary of prioritized key recommendations
Make it sound authentic - Kent Beck should talk about tests, Wadler might make theoretical observations, Niko would focus on Rust idioms, Jung might worry aboutunsafe
usage, Hickey would push for simplicity, etc.
Review these aspects:
- Type design and safety guarantees
- API ergonomics and discoverability
- Performance implications of the abstractions
- Error handling patterns
- Ensure robust security by strictly validating dependencies, sanitizing data, enforcing secure communication, and following best practices to prevent injection, data leaks, and misuse.
- Documentation and examples
- Test coverage and property testing
- Module organization and separation of concerns
- Integration with the broader Rust ecosystem
- Whether the functional approach is appropriate for the domain
Keep the tone collegial but honest - these are experts who respect each other but won't pull punches on technical matters.
Conclusion
Flow coding represents a valuable addition to the code review toolkit, particularly for surfacing language-specific improvements and architectural insights. However, its application in security-critical contexts requires careful consideration of AI limitations and robust verification processes.
The technique excels at generating idiomatic improvements and catching common code quality issues, but security teams must maintain vigilance against AI hallucinations and ensure comprehensive validation of all generated code (doesn’t matter if vibe coded, flow coded or else).
Key Takeaway: Flow coding can enhance code quality when properly integrated with human oversight and security validation processes, but should never replace human judgment in security-critical decisions.
Leave a Reply