Bang means dangerous, question mark means predicate¶
Level: 101 · a first-week reader who writes Python
One line: A trailing ! marks the more dangerous of two versions of a method — usually the one that changes its receiver, and several of those return nil when nothing changed — and a trailing ? marks a method that answers a yes/no question.
Ruby lets ? and ! end a method name, and the community uses them as signals. ? is the easy one: empty?, zero?, include?, frozen? answer a question and return true or false. ! is subtler. It does not mean "mutates" — <<, push, pop and delete all mutate without it — it means "the more dangerous of these two"; when a method comes in a pair, the bang version is the one that changes the receiver (sort!), raises instead of returning nil, or, for exit!, skips the clean-up handlers. Several bang methods on String and Array (strip!, uniq!, sub!, upcase!, flatten!, compact!) also return nil when they had nothing to do, so a chain like s.strip!.upcase works on one input and raises NoMethodError on another.
Verified output of bang_and_question_methods_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. sort returns a new array: a.sort -> [1, 2, 3], a is still [3, 1, 2]
2. sort! changes the receiver: a.sort! -> [1, 2, 3], a is now [1, 2, 3], r.equal?(a) is true
3. nil when nothing changed: [1, 2].uniq! -> nil
"x".strip! -> nil
"abc".sub!("z", "y") -> nil
"ABC".upcase! -> nil
[1, 2].flatten! -> nil
[1, 2].compact! -> nil
the new value when it did: [1, 1].uniq! -> [1]
" x ".strip! -> "x"
4. so chaining a bang breaks: "abc".strip!.upcase -> NoMethodError: undefined method 'upcase' for nil
chain the non-bang versions: " abc ".strip.upcase -> "ABC"
5. ? methods answer a question: [].empty? true, 0.zero? true, [1, 2].include?(2) true, "a".frozen? false, 1.even? false, nil.nil? true
6. ? and ! are part of the name: def ready? -> :ready?, def go! -> :go!, "abc".respond_to?(:strip!) true
7. ! is not always mutation: exit -> stdout "exiting\nat_exit ran\n", status 0
exit! -> stdout "exiting\n", status 1
8. mutation without a bang: b << 3; b.push(4); b.pop -> 4, b is [1, 2, 3]
(the bang marks the dangerous one of a pair; a method with no twin has no bang)
Reading the output¶
Rows 1 and 2 are the pair every reader meets first: sort returns a new array and leaves a alone; sort! sorts a in place and returns a itself (r.equal?(a) is true), so it chains. Row 3 is the trap: uniq!, strip!, sub!, upcase!, flatten! and compact! return nil when nothing changed, and the value only when something did. Row 4 shows the consequence — "abc".strip!.upcase raises NoMethodError: undefined method 'upcase' for nil — and the fix, which is to chain the non-bang versions.
Row 5 is the ? convention in action; row 6 shows the characters are simply part of the name (method(:ready?).name is :ready?). Row 7 is the case that proves ! is about danger, not mutation: two child Rubies each register an at_exit handler and print "exiting"; exit runs the handler and exits 0, exit! skips it — and its default status is 1. Row 8 is the other half of the rule: <<, push and pop change the array without any bang, because they have no gentler twin to be contrasted with.
Compared with Python¶
Python has no naming convention for danger; it has a return convention. A method that changes its receiver returns None — list.sort(), append, extend, reverse, clear — and the copying version is a separate function (sorted, reversed). Rows 1 to 4 line up with Ruby's exactly: sorted(a) copies, a.sort() mutates and returns None, and chaining it (a.sort().append(1)) fails with AttributeError on None the way Ruby's chain failed on nil. The difference is that Python's None comes back always, not only when nothing changed, so there is nothing to signal (row 3): str is immutable, so strip() and upper() always return a string. Row 5: predicates are is_/has_ names (isalpha, isdigit) or plain expressions (len(x) == 0, x in y), and row 6 shows def ready?(): does not parse. Row 7: sys.exit() runs atexit handlers, os._exit(0) skips them — the same pair, without the punctuation to warn you. Row 8: append returns None, pop returns the element; no rule tells you which.
Verified output of bang_and_question_methods_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. sorted returns a new list: sorted(a) -> [1, 2, 3] a is still [3, 1, 2]
2. sort() changes the receiver: a.sort() -> None a is now [1, 2, 3]
3. nothing-changed returns: [1, 2].sort() -> None (None whether or not anything changed)
'x'.strip() -> 'x'
'abc'.replace('z','y') -> 'abc'
'ABC'.upper() -> 'ABC' (str is immutable: always a string)
[1, 2].reverse() -> None
[1, 2].clear() -> None
the new value when it did: sorted([1, 1]) -> [1, 1] (a copy, so nothing to signal)
' x '.strip() -> 'x'
4. so chaining sort() breaks: a.sort().append(1) -> AttributeError: 'NoneType' object has no attribute 'append'
chain the copying versions: ' abc '.strip().upper() -> 'ABC'
5. predicates are is_/has_ names: len([]) == 0 True 0 == 0 True 2 in [1, 2] True 'a'.isalpha() True 1 % 2 == 0 False None is None True
6. ? and ! do not parse: def ready?(): -> SyntaxError ; def go!(): -> SyntaxError ; hasattr(str, 'strip') True
7. the dangerous exit: sys.exit() -> stdout 'exiting\natexit ran\n', status 0
os._exit(0) -> stdout 'exiting\n', status 0
8. mutation returns None: b.append(3) -> None ; b.pop() -> 4 ; b is [1, 2, 3]
(no naming rule: append, extend, remove, reverse all return None)
Try it¶
- Write
normalize!(s)that strips and downcasessin place usingstrip!anddowncase!on separate lines and returnss. Show it on" MiXed "and on"clean". Then write the naive chained versions.strip!.downcase!and show what it does on"clean", on" MiXed "and on" mixed ". - Write a
Basketclass withadd(x)returningself(sobasket.add(1).add(2)chains),add!(x)that raises when the basket already holds three items, andfull?. Print a chain,full?before and after, and the class of the exceptionadd!raises.
Solution to 1
bang_and_question_methods_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Kata: normalize a string in place without chaining bang methods, then show
# why the chained version is a trap.
def normalize!(s)
s.strip!
s.downcase!
s
end
def normalize_chained!(s)
s.strip!.downcase!
end
a = " MiXed "
puts "1. normalize!(\" MiXed \") -> #{normalize!(a).inspect}, and a is now #{a.inspect}"
b = "clean"
puts "2. normalize!(\"clean\") -> #{normalize!(b).inspect} (nothing changed, still returns the string)"
c = "clean"
begin
normalize_chained!(c)
rescue NoMethodError => e
puts "3. normalize_chained!(\"clean\") -> #{e.class}: #{e.message}"
end
d = " MiXed "
puts "4. normalize_chained!(\" MiXed \") -> #{normalize_chained!(d).inspect} (works only because strip! changed something)"
e = " mixed "
puts "5. normalize_chained!(\" mixed \") -> #{normalize_chained!(e).inspect} (strip! changed it, downcase! did not, and e is #{e.inspect})"
Verified output of bang_and_question_methods_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. normalize!(" MiXed ") -> "mixed", and a is now "mixed"
2. normalize!("clean") -> "clean" (nothing changed, still returns the string)
3. normalize_chained!("clean") -> NoMethodError: undefined method 'downcase!' for nil
4. normalize_chained!(" MiXed ") -> "mixed" (works only because strip! changed something)
5. normalize_chained!(" mixed ") -> nil (strip! changed it, downcase! did not, and e is "mixed")
See also¶
- Operators are methods, except the ones that are not —
!is a method too, and<<is the mutator that never needed a bang &.skips the call when the receiver is nil — what anilfromstrip!does to the next call in a chain- The Enumerable toolbox —
select/reject/mapand their bang twins on Array - Sort stability and sort_by —
sortvssort!and what neither guarantees - Style and naming — the conventions in one place:
?,!,snake_case,CamelCase - Exit, at_exit and SystemExit — row 7 in full:
exit,exit!,abortand the handlers - Python: sorting is not comparing ↗ — what
sorteddoes with akey=, on the Python side - Strings are mutable;
<<keeps the object — A RubyStringis a mutable object - Twenty-three Ruby traps for a Python programmer — The twenty-three places where a Python reflex gives the wrong answer in Ruby
- Twenty-three Python traps for a Ruby programmer — The twenty-three places where a Ruby reflex gives the wrong answer in Python
- There is more than one way, and one obvious way — Ruby's idioms