Skip to content

09 — Control flow and pattern matching

One line: Every Ruby construct is an expression with a value, case comes in two forms — when asks === and in matches a shape — and the loops that open no scope are the ones Ruby programmers avoid.

This chapter is about the statements a Python programmer already knows — if, while, for, and/or, a switch of some kind — and what Ruby does differently with each. The first three lessons are the grammar: there are no statements, only expressions, so x = if … end and def returning a Symbol are ordinary; unless, until and the trailing modifiers are the Perl-flavoured spellings (Not machine-checked here.), with begin … end while as the do-while; and and/or are a second pair of boolean operators that bind below =, which is the trap behind x = a or b. The middle five are case. case/when sorts by ===, a method every class answers in its own way — class membership, range cover, regex match, a lambda call — and case/in (2.7) matches shapes: array patterns, hash patterns, pins, guards, alternatives, and the two methods, deconstruct and deconstruct_keys, that let any object take part, with NoMatchingPatternError when nothing fits. The last lesson explains why Ruby code says each and not for: a for loop is each without the scope.

For a Python reader the model carries over more often than not. Python's match/case (3.10) has the same four families of pattern — sequence, mapping, class and capture — the same rule that a bare name binds, guards with if, and alternatives with |; for and while leak their variables in both languages; and/or return an operand in both. It breaks in the details, and every page measures them: Python has statements with no value and three expression-level crossings (x if c else y, :=, lambda); one pair of boolean operators with and above or; no unless, until, modifiers or do-while, but a for … else Ruby lacks; a match that silently does nothing when no case fits, where Ruby raises; __match_args__ and Sequence/Mapping registration where Ruby has two duck-typed methods; and no pin, with dotted names as value patterns instead. Each lesson's Python twin prints the same numbered rows as the Ruby program, so the two can be read side by side.

Lesson Level The one thing
Everything is an expression, even def and class 101 if, case, begin, while, def and class all evaluate to a value; Python has statements and three crossings
unless negates if; modifiers hang off the end of a line 101 unless, until, x if c, begin … end while (the do-while), loop do, and how elsif must be spelled
and/or bind lower than = 201 x = false or true assigns false; and/or share one precedence; ||= keeps 0 and ""; comparisons do not chain
case/when asks ===, and every class answers it its own way 201 one case sorts by class, Range, Regexp, lambda and splat, with no fall-through and a value at the end
case/in matches shape, not just value 201 array, hash, nested and find patterns, => on one line, in as a boolean, and what Python's match lacks
A pin compares; a bare name binds 201 in expected matches anything; ^expected compares; guards, \| alternatives and the two syntax errors
deconstruct and deconstruct_keys let any object be matched 301 two duck-typed methods, the keys argument, Struct/Data/MatchData for free; Python's __match_args__
Nothing matched: case/in raises, case/when returns nil 201 NoMatchingPatternError and NoMatchingPatternKeyError#key; Python's match is silent unless you raise
for loops do not make a scope 201 for and while leak every variable; a block does not; lambdas made in a for share one variable

Read more