tap returns the receiver, then returns the block's value¶
Level: 201 · anyone who has broken a method chain to insert a puts
One line: x.tap { … } yields x to the block and returns x whatever the block returned, so a debugging puts can sit inside a chain; x.then { … } yields x and returns the block's value, so a value can flow through a pipeline of steps — and Python has neither, only a call, a two-line helper, or functools.reduce.
Both methods live in Kernel, so every object has them. tap is for side effects: log, inspect, assert, and the chain continues with the same object. then (an alias of the older yield_self) is for transformation: "5".then(&:to_i) is a function application written left to right, and a series of then calls reads as a pipeline. With a lambda in hand, >> composes the same pipeline into one callable. The program prints the two methods on a number, a chain with two tap stops, the leading-dot layout that Ruby allows across lines, itself as the identity, two spellings of the same pipeline, and the fact that tap discards the block's return value but keeps its mutations.
Verified output of tap_then_and_chaining_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. 5.tap { |v| v * 100 } 5
2. 5.then { |v| v * 100 } 500
-- tap inside a chain, printing without breaking it --
before sort: [3, 1, 2]
after sort: [1, 2, 3]
3. [3, 1, 2].tap {..}.sort.tap {..}.map { * 10 } [10, 20, 30]
4. "5".then(&:to_i) 5 (Integer)
5. 5.then { it + 1 } 6
6. leading-dot chain over four lines, summed 10
7. [1, 2, 2].group_by(&:itself) {1 => [1], 2 => [2, 2]}
8. " Hello World ".then(&strip).then(&lower).then(&snake) "hello_world"
9. (strip >> lower >> snake).call(same string) "hello_world"
10. 5.method(:then) == 5.method(:yield_self) true
11. 5.method(:tap).owner / 5.method(:then).owner Kernel / Kernel
12. [].tap { |a| a << 1; :ignored } [1] (mutation kept, block value dropped)
Reading the output¶
- Rows 1–2 are the whole difference: the same block,
v * 100, andtaphands back5whilethenhands back500. - Row 3: the two indented lines were printed by
tapblocks in the middle of the chain; the chain still ended in[10, 20, 30], because eachtapreturned the array it was given. - Rows 4–5:
thenwith a symbol-to-proc and with theitparameter."5".then(&:to_i)isto_iapplied to"5", and the result is anInteger. - Row 6: a chain may start each line with
.method— Ruby continues the expression when the next line begins with a dot. - Row 7:
itselfreturns its receiver, which is whatgroup_byneeds when the key is the element. - Rows 8–9: a pipeline as three
thencalls, then as one composed lambda built with>>. Same input, same output. - Rows 10–11:
thenandyield_selfare one method under two names, and bothtapandthenare owned byKernel, so they exist on every object. - Row 12:
tapthrew away:ignoredbut the<<inside the block still happened, because the block worked on the receiver itself.
Compared with Python¶
Python has no tap and no then. A call is then — (lambda v: v * 100)(5) — and an inline function application reads right to left, int("5"), rather than left to right. tap is a two-line helper (row 1) or, inside an expression, the tuple trick (print(x), x)[1]. Row 3 nests the helper inside sorted inside a comprehension, and the two debug lines still print in the same order, but the code reads inside-out. Row 6 shows the Python layout for a multi-line pipeline: a generator expression inside parentheses, with the steps as nested generators. Row 7 measures that there is no identity function in operator, so lambda x: x is written by hand. Rows 8–9 are the pipeline as a functools.reduce over a tuple of functions, once as a pipe call and once as a compose that returns a callable. Row 10 measures the absence: hasattr(5, "then") is False. Row 11 is Python's real chaining idiom — methods on immutable strings return new strings, so .strip().lower().replace() chains naturally, while row 12 shows why list chains do not: append returns None, not the list.
Verified output of tap_then_and_chaining_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. tap(5, lambda v: v * 100) (helper) 5
2. (lambda v: v * 100)(5) (a call is `then`) 500
-- tap inside a chain, printing without breaking it --
before sort: [3, 1, 2]
after sort: [1, 2, 3]
3. [v * 10 for v in tap(sorted(tap(xs, ..)), ..)] [10, 20, 30]
4. int("5") 5 (int)
5. (lambda it: it + 1)(5) 6
6. generator pipeline over four lines, summed 10
7. groupby(sorted(xs)); operator.identity exists? {1: [1], 2: [2, 2]} False
8. pipe(" Hello World ", strip, lower, snake) 'hello_world'
9. compose(strip, lower, snake)(same string) 'hello_world'
10. hasattr(5, 'then') / hasattr(5, 'tap') False / False
11. " Hello World ".strip().lower().replace(" ", "_") 'hello_world'
12. seen = []; seen.append(1) returns None; seen is [1] (no receiver comes back)
Try it¶
- Take the array
%w[pear fig banana kiwi apple plum], and build a three-step chain —selectwords longer than three letters,mapto upper case,sort— with atapafter each step that prints the intermediate array. Print the final result and confirm it is what the lasttapsaw. - Write
pipeline(*steps)that takes lambdas and returns one lambda composed with>>. Run" hello world "throughstrip,squeeze(" ")andcapitalize, once with yourpipelineand once with a chain ofthen, and print both results.
Solution to 1
tap_then_and_chaining_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Kata: a three-step chain with a `tap` after each step, so every intermediate
# collection is printed and the chain still returns its final value.
words = %w[pear fig banana kiwi apple plum]
result = words
.select { |w| w.length > 3 }
.tap { |ws| puts "after select: #{ws.inspect}" }
.map(&:upcase)
.tap { |ws| puts "after map: #{ws.inspect}" }
.sort
.tap { |ws| puts "after sort: #{ws.inspect}" }
puts "returned: #{result.inspect}"
puts "same object as the last tap saw? #{result.equal?(result.tap { |r| r })}"
Verified output of tap_then_and_chaining_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
- Symbol to proc — the
&:to_iin row 4, explained - Curry and composition — the
>>that built row 9's pipeline - Indentation is convention in Ruby, syntax in Python — where the leading-dot chain of row 6 fits in the style guide
- There is more than one way, and one obvious way —
thenandyield_selfas one of Ruby's many aliases