Just yesterday I suddenly understood that there is a small neat trick, allowing to provide friendly error messages for missing method parameters:

# default way to do things:
def read_data(file)
  # ...

read_data('1.txt') # => works
# wrong number of arguments (given 0, expected 1) (ArgumentError)
# ^^ not really friendly

def read_data(file:)
  # ...

# missing keyword: file (ArgumentError)
# ^^ at least some hint of what was expected

# But how about this?
def read_data(file = raise(ArgumentError, '#read_data requires path to .txt file with data in proper format'))
  # ...

# #read_data requires path to .txt file with data in proper format (ArgumentError)
# ^^ isn't it nice?..

Of course, it is not that useful with a simple method with one argument, but for complicated APIs with several keyword args, it might be of some use. But what I was pleasantly surprised with is how simple it is—and that it works.

How it works?

The argument = raise(...) is not some separate Ruby feature, but it is a natural consequence of two facts:

  1. You can put any Ruby expression as the argument’s default value, and it will be evaluated in the same context as the method’s body, on each method call (when the argument is not provided)
  2. raise is just a method, not some special syntax, and like any other method call, it is an expression and can be put as an argument’s default value.

“Any expression”? Really?


You can do even this (though you probably shouldn’t!):

def read_data(file = begin
                       puts "Using default argument"
                       if Time.now.hour < 12
  puts "Reading #{file}"

# Prints:
#  Using default argument
#  Reading evening.txt

As was already said above, the context of evaluation is the same as for method body, and all default values are evaluated sequentially, so you can do this (and probably shouldn’t!):

class ArgsTracker
  attr_reader :args

  def initialize
    @args = []

  def track(
    a: begin; args << :a; 100 end,
    b: begin; puts "a was #{a}"; args << :b end)

tracker = ArgsTracker.new

# Prints: "a was 100", and adds [:a, :b] to tracker

tracker.track(a: 5)
# Prints: "a was 5", and adds only [:b] (which was not provided) to tracker

tracker.args # => [:a, :b, :b]

Cool. Ugly, but cool.

How is this useful?

The fact that default values are calculated on each call, and in the context of called class, have some simple and useful consequences. Probably you already have seen and used some of them:

def log(something, at: Time.now) # will be calculated at each call of log, when alternative at: is not provided

def setup_output(out: $stdout, err: $stderr, warn: out) # default output device for warn would be always
                                                        # the same as `out`
  # ...

class A
  def process(order: default_order) # will call the same object's method to calculate default


  def default_order
    # some complicated calculation, depending on the object's state

More advanced usage

Besides the example from which we have started, one might think about other relatively sane but not very simple usages of the on-the-fly calculation, for example, tracking of default values usage (might be useful on legacy refactoring, when we aren’t sure whether defaults are used at all, but can’t allow ourselves to just break the codebase):

def log_default(name, value)
  # or logger.debug
  puts "#{caller.first}: default value for #{name} was invoked from #{caller[2]}"

# Now change this:
def some_method(factor: 100)
#...to this:
def some_method(factor: log_default(:factor, 100))

# ...and...
# Logs:
#   ...in `some_method': default value for factor was invoked from `some_other_method'

One might also imagine my initial example (with fail) extended for some very friendly API to use like fun(arg: friendly_fail(:arg)), which fetches large explanatory string from constant/i18n config, enriches it with calling context (like, “if caller contains this, we are saying this shouldn't be called from <framework>”) and raises Very Friendly Exception.

Not you should do something like this anytime soon, but rather “it is interesting that you can, and probably someday you’d like to try”.

Have fun!