I needed to parse a small custom config format this week, a few key/value lines with nested blocks, and rather than reach for split and regret it, I used nom. I'd avoided parser combinators for years because the type signatures look like someone fell on the keyboard. Spending an evening with them, I get it now.
The mental model that unlocked it: a parser is just a function that takes a slice of input and returns the remaining input plus whatever it parsed. nom gives you tiny parsers, tag, take_while, digit1, and combinators to glue them together, tuple, alt, delimited, many0. You build a parser for a key, a parser for a value, then combine them into a parser for a line, then a parser for a block.
fn key_value(input: &str) -> IResult<&str, (&str, &str)> {
separated_pair(
alphanumeric1,
delimited(space0, char('='), space0),
is_not("\n"),
)(input)
}
The payoff is that errors compose too. When the input doesn't match, nom tells you where, and you get that without writing a single index-juggling loop. The cost is the learning curve and the type ergonomics, which are genuinely awkward until they suddenly aren't. For anything beyond splitting on a delimiter, though, I'll reach for it again. It is far less error-prone than the hand-rolled string surgery it replaced.