Skip to content

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, and tap hands back 5 while then hands back 500.
  • Row 3: the two indented lines were printed by tap blocks in the middle of the chain; the chain still ended in [10, 20, 30], because each tap returned the array it was given.
  • Rows 4–5: then with a symbol-to-proc and with the it parameter. "5".then(&:to_i) is to_i applied to "5", and the result is an Integer.
  • Row 6: a chain may start each line with .method — Ruby continues the expression when the next line begins with a dot.
  • Row 7: itself returns its receiver, which is what group_by needs when the key is the element.
  • Rows 8–9: a pipeline as three then calls, then as one composed lambda built with >>. Same input, same output.
  • Rows 10–11: then and yield_self are one method under two names, and both tap and then are owned by Kernel, so they exist on every object.
  • Row 12: tap threw away :ignored but 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

  1. Take the array %w[pear fig banana kiwi apple plum], and build a three-step chain — select words longer than three letters, map to upper case, sort — with a tap after each step that prints the intermediate array. Print the final result and confirm it is what the last tap saw.
  2. Write pipeline(*steps) that takes lambdas and returns one lambda composed with >>. Run " hello world " through strip, squeeze(" ") and capitalize, once with your pipeline and once with a chain of then, 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.

after select: ["pear", "banana", "kiwi", "apple", "plum"]
after map:    ["PEAR", "BANANA", "KIWI", "APPLE", "PLUM"]
after sort:   ["APPLE", "BANANA", "KIWI", "PEAR", "PLUM"]
returned:     ["APPLE", "BANANA", "KIWI", "PEAR", "PLUM"]
same object as the last tap saw? true

See also