089 — Bob
Tutorial Video
Text description (accessibility)
This video demonstrates the "089 — Bob" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Implement a conversational responder named Bob. Key difference from OCaml: | Aspect | Rust | OCaml |
Tutorial
The Problem
Implement a conversational responder named Bob. Bob replies based on the characteristics of the input: silence gets "Fine. Be that way!", yelling gets "Whoa, chill out!", a question gets "Sure.", a yelled question gets "Calm down, I know what I'm doing!", and anything else gets "Whatever." Compare both a tuple-match and an if/else implementation.
🎯 Learning Outcomes
.trim(), .ends_with('?'), and .chars().any(…) for string inspectionis_yelling by checking both letter presence and full uppercase equality(is_silence, is_yelling, is_question) for clear dispatch&'static str for response literals — no allocation neededString module functionsCode Example
#![allow(clippy::all)]
/// Bob — String Pattern Matching
///
/// Ownership: Input is borrowed &str. Responses are &'static str (no allocation).
fn is_question(s: &str) -> bool {
s.trim().ends_with('?')
}
fn is_yelling(s: &str) -> bool {
let has_letter = s.chars().any(|c| c.is_alphabetic());
has_letter && s == s.to_uppercase()
}
fn is_silence(s: &str) -> bool {
s.trim().is_empty()
}
pub fn response_for(s: &str) -> &'static str {
match (is_silence(s), is_yelling(s), is_question(s)) {
(true, _, _) => "Fine. Be that way!",
(_, true, true) => "Calm down, I know what I'm doing!",
(_, true, false) => "Whoa, chill out!",
(_, false, true) => "Sure.",
_ => "Whatever.",
}
}
/// Version 2: Using if-else chain (more readable for some)
pub fn response_for_v2(s: &str) -> &'static str {
let trimmed = s.trim();
if trimmed.is_empty() {
"Fine. Be that way!"
} else if is_yelling(s) && is_question(s) {
"Calm down, I know what I'm doing!"
} else if is_yelling(s) {
"Whoa, chill out!"
} else if is_question(s) {
"Sure."
} else {
"Whatever."
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_yelling() {
assert_eq!(response_for("WATCH OUT!"), "Whoa, chill out!");
}
#[test]
fn test_question() {
assert_eq!(response_for("Does this work?"), "Sure.");
}
#[test]
fn test_yelling_question() {
assert_eq!(
response_for("WHAT IS THIS?"),
"Calm down, I know what I'm doing!"
);
}
#[test]
fn test_silence() {
assert_eq!(response_for(" "), "Fine. Be that way!");
}
#[test]
fn test_normal() {
assert_eq!(response_for("Hi"), "Whatever.");
}
#[test]
fn test_v2_matches() {
for s in &[
"WATCH OUT!",
"Does this work?",
"WHAT IS THIS?",
" ",
"Hi",
] {
assert_eq!(response_for(s), response_for_v2(s));
}
}
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Uppercase check | s == s.to_uppercase() | String.uppercase_ascii s = s |
| Letter presence | .chars().any(c.is_alphabetic()) | Seq.exists (fun c -> …) |
| Trim | .trim() | String.trim |
| Last char | .ends_with('?') | String.get s (len - 1) = '?' |
| Dispatch | match (bool, bool, bool) | match …, …, … tuple |
| Response type | &'static str | string |
The tuple match is a clean alternative to nested if/else chains. Exhaustive checking ensures no case is forgotten — adding a new boolean flag forces updating every match arm. The &'static str return type for constant responses is a best practice: avoid allocating when the string is already in the binary.
OCaml Approach
OCaml checks is_question by indexing the last character of the trimmed string. is_yelling uses Seq.exists on String.to_seq for letter presence and String.uppercase_ascii for comparison. response_for pattern-matches on the same triple. The logic is identical; OCaml's string API is more functional (String.to_seq, Seq.exists) whereas Rust uses method calls on &str.
Full Source
#![allow(clippy::all)]
/// Bob — String Pattern Matching
///
/// Ownership: Input is borrowed &str. Responses are &'static str (no allocation).
fn is_question(s: &str) -> bool {
s.trim().ends_with('?')
}
fn is_yelling(s: &str) -> bool {
let has_letter = s.chars().any(|c| c.is_alphabetic());
has_letter && s == s.to_uppercase()
}
fn is_silence(s: &str) -> bool {
s.trim().is_empty()
}
pub fn response_for(s: &str) -> &'static str {
match (is_silence(s), is_yelling(s), is_question(s)) {
(true, _, _) => "Fine. Be that way!",
(_, true, true) => "Calm down, I know what I'm doing!",
(_, true, false) => "Whoa, chill out!",
(_, false, true) => "Sure.",
_ => "Whatever.",
}
}
/// Version 2: Using if-else chain (more readable for some)
pub fn response_for_v2(s: &str) -> &'static str {
let trimmed = s.trim();
if trimmed.is_empty() {
"Fine. Be that way!"
} else if is_yelling(s) && is_question(s) {
"Calm down, I know what I'm doing!"
} else if is_yelling(s) {
"Whoa, chill out!"
} else if is_question(s) {
"Sure."
} else {
"Whatever."
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_yelling() {
assert_eq!(response_for("WATCH OUT!"), "Whoa, chill out!");
}
#[test]
fn test_question() {
assert_eq!(response_for("Does this work?"), "Sure.");
}
#[test]
fn test_yelling_question() {
assert_eq!(
response_for("WHAT IS THIS?"),
"Calm down, I know what I'm doing!"
);
}
#[test]
fn test_silence() {
assert_eq!(response_for(" "), "Fine. Be that way!");
}
#[test]
fn test_normal() {
assert_eq!(response_for("Hi"), "Whatever.");
}
#[test]
fn test_v2_matches() {
for s in &[
"WATCH OUT!",
"Does this work?",
"WHAT IS THIS?",
" ",
"Hi",
] {
assert_eq!(response_for(s), response_for_v2(s));
}
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_yelling() {
assert_eq!(response_for("WATCH OUT!"), "Whoa, chill out!");
}
#[test]
fn test_question() {
assert_eq!(response_for("Does this work?"), "Sure.");
}
#[test]
fn test_yelling_question() {
assert_eq!(
response_for("WHAT IS THIS?"),
"Calm down, I know what I'm doing!"
);
}
#[test]
fn test_silence() {
assert_eq!(response_for(" "), "Fine. Be that way!");
}
#[test]
fn test_normal() {
assert_eq!(response_for("Hi"), "Whatever.");
}
#[test]
fn test_v2_matches() {
for s in &[
"WATCH OUT!",
"Does this work?",
"WHAT IS THIS?",
" ",
"Hi",
] {
assert_eq!(response_for(s), response_for_v2(s));
}
}
}
Deep Comparison
Bob — Comparison
Core Insight
Bob demonstrates tuple pattern matching on computed boolean conditions. The approach is identical in both languages — compute predicates, match on the tuple. String operations differ slightly in ergonomics.
OCaml Approach
String.trim, String.uppercase_ascii for string opsString.to_seq |> Seq.exists for character testingString.get s (String.length s - 1)match a, b, c with | true, _, _ -> ...Rust Approach
.trim(), .to_uppercase() — method syntax.chars().any(|c| c.is_alphabetic()) for character testing.ends_with('?') — dedicated methodmatch (a, b, c) { (true, _, _) => ... }Comparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Trim | String.trim | .trim() |
| Uppercase | String.uppercase_ascii | .to_uppercase() |
| Ends with | Manual char check | .ends_with() |
| Has letter | Seq.exists | .chars().any() |
| Tuple match | match a, b, c with | match (a, b, c) |
| Result type | string | &'static str |
Learner Notes
.ends_with() is more ergonomic than OCaml's manual index check&'static str for constant strings avoids allocation.is_alphabetic() handles Unicode; OCaml's manual check doesn'tExercises
is_polite(s: &str) -> bool predicate that checks for "please" and integrate it into the response logic.String (allocated) to allow parameterised responses (e.g. "Sure, {name}.").is_yelling.response_for to also detect silence made of only punctuation (e.g. "...") and treat it as silence.