DEV Community

Cover image for Four things about Ruby method arguments that still catch experienced developers
Figsy
Figsy

Posted on

Four things about Ruby method arguments that still catch experienced developers

I've written Ruby for years. Recently I sat down and worked through method arguments properly — not "I know what *args does", but end to end, every form, every edge. A few things genuinely surprised me.

Here are four. All of them run on Ruby 3.2+.

1. A splat doesn't have to go last

Most of us learn *args as "and everything else, at the end". It isn't.

def log(time, level, *words, source)
  [time, level, words, source]
end

log("12:00", :warn, "disk", "almost", "full", "kernel")
# => ["12:00", :warn, ["disk", "almost", "full"], "kernel"]
Enter fullscreen mode Exit fullscreen mode

Ruby matches the fixed parameters at both ends first, then gathers whatever's left into the middle. Useful when the ends of a variable-length list mean something different from the middle.

2. The classic closure "fix" doesn't fix anything

You've probably hit the closure-capture trap, where every lambda sees the loop variable's final value. And you probably know the fix: capture it in a local.

cs = []
i = 0
while i < 3
  j = i          # "capture it in a local"
  cs << -> { j }
  i += 1
end

cs.map(&:call)   # => [2, 2, 2]
Enter fullscreen mode Exit fullscreen mode

Still wrong. while doesn't open a new scope on each pass, so j is one variable, reused. The real fix is a block, whose parameter is fresh each iteration:

cs = []
3.times { |i| cs << -> { i } }
cs.map(&:call)   # => [0, 1, 2]
Enter fullscreen mode Exit fullscreen mode

3. **nil forbids the syntax, not the hash

**nil says "this method takes no keyword arguments". Sort of.

def path(*segments, **nil) = segments

path("a", x: 1)      # ArgumentError — good
path("a", { x: 1 })  # => ["a", {:x=>1}] — straight through
Enter fullscreen mode Exit fullscreen mode

It rejects keyword syntax. An explicit hash is just a positional argument, and it sails past. Still worth using: without it, a lone *segments silently swallows a trailing x: 1 as a hash, and you find out much later.

4. Ruby always passes two arguments to respond_to_missing?

If you pair method_missing with respond_to_missing? — and you should — the second parameter isn't decoration.

def respond_to_missing?(name)   # one parameter
  true
end

obj.respond_to?(:x)   # ArgumentError: given 2, expected 1
Enter fullscreen mode Exit fullscreen mode

You passed one argument. Ruby passed two. It always passes two — respond_to_missing?(:x, false) — because respond_to? forwards its private-methods flag straight through. The conventional signature is (name, include_private = false), and if you never read the flag, (name, *) works too.


Why any of this matters?

None of this is advanced. It's the code you write every day — the part between def and the first line of the method. It's also where generated code is often wrong in ways that look fine, so it's worth knowing well when you're reviewing something you didn't write.

I went far enough down this hole that it became a book: Ruby Method Arguments: From Your First Call to Metaprogramming. It starts at passing one value to a method and ends with building a small DSL — closures, currying, method_missing, instance_eval, and rebuilding the patterns behind Rails and RSpec along the way.

What else in this corner of Ruby has caught you out? I'd like to hear it.

Top comments (0)