skip to content

In Ruby, what do the >> and << operators return for Proc and Method objects, and which callable runs first in each?

level: middleimportance: should knowfreq 30%

answer

  1. f >> g: f first
  2. f << g: g first
  3. result is always a Proc
  4. any object with call
  5. TypeError: callable object is expected

basics

~20 s

Both return a new Proc. f >> g calls f with the arguments, then g with f's result; f << g calls g first, then f. The operand may be a Proc, a Method or any object responding to call.

solid answer

~40 s

`>>` and `<<` are defined on `Proc` and `Method` and compose two callables into a new `Proc`, even when the receiver is a `Method`. `f >> g` reads left to right: the new proc calls `f` with all its arguments, then calls `g` with `f`'s single result. `f << g` reads right to left: `g` runs first and `f` receives its result. The operand may be a `Proc`, a `Method` or any object that responds to `call`, such as a class with a `self.call`; anything else raises `TypeError` (`callable object is expected`). Only the first callable to run receives the original arguments; the second always receives exactly one value. The result is a lambda when the first callable to run is a lambda, a `Method` or another non-proc callable.

code

ruby · 16 lines
ruby
double = ->(x) { x * 2 }
inc    = ->(x) { x + 1 }

(double >> inc).call(5)   # => 11, double first
(double << inc).call(5)   # => 12, inc first

class Upcaser
  def self.call(text) = text.upcase
end

label = method(:String) >> Upcaser >> ->(s) { "[#{s}]" }
label.call(:total)        # => "[TOTAL]"
label.class               # => Proc
label.lambda?             # => true

double >> 42              # TypeError: callable object is expected

go deeper

for a junior

Recall that >> runs the left callable first and << runs the right one first, and that both produce a new proc.

for a middle

Explain that operands can be Procs, Methods or any object with call, that the second step gets one value, and that TypeError guards non-callables.

for a senior

Build readable formatting pipelines from tested steps, keep one composition direction, and account for lambda-ness and the single-value handoff.

for a principal

Decide whether composed callables or explicit objects make pipelines easier to debug, trace and extend for the team.

## Composition in Ruby **Function composition** builds one callable out of two by feeding the output of one into the other. Ruby provides it as operators on `Proc` and `Method`: | Expression | Runs first | Then | Reads | |---|---|---|---| | `f >> g` | `f` with the arguments | `g` with `f`'s result | left to right | | `f << g` | `g` with the arguments | `f` with `g`'s result | right to left | With `double = ->(x) { x * 2 }` and `inc = ->(x) { x + 1 }`: - `(double >> inc).call(5)` computes `inc.(double.(5))`, which is `11`. - `(double << inc).call(5)` computes `double.(inc.(5))`, which is `12`. ## What comes back 1. The result is always a new **`Proc`**. `method(:parse) >> method(:format_row)` returns a `Proc`, not a `Method`, because `Method#>>` converts the method with `to_proc` first. 2. The **first callable to run** receives every argument the composed proc is called with, including keywords and a block. 3. The **second callable** receives exactly one argument: the first one's return value. If you need to pass several values along, return an Array or a small object. 4. **Lambda-ness** follows the callable that runs first. For `f >> g` it is copied from `f`; for `f << g` it comes from `g` when `g` is a `Proc`, and a `Method` or other callable counts as a lambda. Composing two lambdas therefore gives a proc whose `lambda?` is `true`. ## What may be composed The operand can be: - another **`Proc`** or lambda, - a **`Method`** object such as `File.method(:read)`, - **any object that responds to `call`**, for example a class with `def self.call(text)`. The `Proc#>>` documentation shows a pipeline built exactly this way: `File.method(:read) >> Parser >> proc { |data| ... }`. Passing something with no `call` method, such as an Integer or a String, raises `TypeError` with `callable object is expected` when the composition is built, not when it is later called. ## A report pipeline For a report generator, composition turns small steps into one formatter: ```ruby normalize = ->(row) { row.transform_values { |v| v.to_s.strip } } render = PriceSheet.new("EUR").method(:format_row) terminate = ->(line) { line + "\n" } formatter = normalize >> render >> terminate ReportGenerator.new(formatter: formatter) ``` Each step can be tested alone, and the pipeline reads in the order the data flows. Choose one direction for a codebase; `>>` reads in execution order, which many teams find easier to follow. ## Composition versus a hand-written lambda | Aspect | `normalize >> render >> terminate` | `->(row) { terminate.(render.(normalize.(row))) }` | |---|---|---| | Reads in data-flow order | Yes | Inside out | | Adding a step | One more `>>` | Another level of nesting | | Passing extra values between steps | Not possible, one value per handoff | Possible | The hand-written lambda is the fallback when a later step needs more than the previous step's result, such as the row index. ## Pitfalls - Mixing up the direction of `<<` and `>>`, which silently applies steps in the wrong order when both steps accept the same type. - Expecting the second step to receive the original arguments as well; it receives only the first step's result. - Composing a step that returns `nil` on bad input: the next step receives `nil` and fails far from the cause. - Building the composition inside a loop; build it once and reuse it.

  • In f >> g, what does g receive if f returns an Array?
    It receives that Array as its single argument. The composed proc passes exactly one value to the second callable. A lambda `g` declared with two parameters raises `ArgumentError`, while a plain proc with two parameters would auto-splat the Array.
  • Can a class be used directly as a step in a composition?
    Yes, if it responds to `call`. A class that defines `def self.call(text)` can appear as `step >> Parser`, because composition accepts any object with a `call` method, not only `Proc` and `Method` instances.

saying these in an interview costs you the question

  • f >> g runs g first and then f
  • Method#>> returns a Method object
  • Both composed callables receive the original arguments
  • Only Proc objects can be composed
  • A non-callable operand fails only when the composition is called