In Progress
Unit 1, Lesson 1
In Progress

Parameter Default

Video transcript & code

Let's say we have some classes representing orders. An order has a subtotal and an address. We also have a class for making calculations about orders called, unsurprisingly, SaleCalculator. It currently has one method, #total, that takes an order and a tax rate and returns a total price.

We can construct an order for an even $100, including a destination address. Then we can use a SaleCalculator to work out the total for the order. Note that for the sake of a simple demo we're using floating point numbers to represent amounts of money. In a production program we'd want to avoid using floats for currency.

Address = Struct.new(:street, :city, :state)

Order = Struct.new(:subtotal, :address)

class SaleCalculator
  def total(order, tax_rate)
    order.subtotal + order.subtotal * tax_rate
  end
end

order = Order.new(100.0, Address.new("123 Main St.", "Anytown", "PA"))

calc = SaleCalculator.new

calc.total(order, 0.06)         # => 106.0

At some point we realize that most of our calculations use the same tax rate, so for ease of use we decide to make the tax_rate parameter optional. To do this, we can use a default parameter. We add an equals sign after the parameter name, followed by the value it should default to.

Now we can omit the tax rate when sending the #total message, and still get a tax-included total price.

Address = Struct.new(:street, :city, :state)

Order = Struct.new(:subtotal, :address)

class SaleCalculator
  def total(order, tax_rate = 0.06)
    order.subtotal + order.subtotal * tax_rate
  end
end

order = Order.new(100.0, Address.new("123 Main St.", "Anytown", "PA"))

calc = SaleCalculator.new

calc.total(order)               # => 106.0

But we hate embedding magic numbers in our code. So we move the default tax rate into a class constant, and reference that constant as the parameter default.

Address = Struct.new(:street, :city, :state)

Order = Struct.new(:subtotal, :address)

class SaleCalculator
  DEFAULT_TAX_RATE = 0.06

  def total(order, tax_rate = DEFAULT_TAX_RATE)
    order.subtotal + order.subtotal * tax_rate
  end
end

order = Order.new(100.0, Address.new("123 Main St.", "Anytown", "PA"))

calc = SaleCalculator.new

calc.total(order)               # => 106.0

This code shows that we can refer to constants defined in the current class inside a parameter default. But what else can we put in a default value?

We soon get an excuse to find out. As our shipping business expands, we find that we really want to have the tax rate for an order determined automatically, based on where the order is headed. So we define a table mapping states to default tax rates. Then we use that table in our tax rate default value, looking up the order's state and using the associated rate if one is not explicitly provided.

We can change our order's state and see the different default tax rate reflected in the result.

Address = Struct.new(:street, :city, :state)

Order = Struct.new(:subtotal, :address)

class SaleCalculator
  TAX_RATES = {
    "PA" => 0.06,
    "CA" => 0.075
    # ...
  }

  def total(order, tax_rate = TAX_RATES[order.address.state])
    order.subtotal + order.subtotal * tax_rate
  end
end

order = Order.new(100.0, Address.new("123 Main St.", "Anytown", "CA"))

calc = SaleCalculator.new

calc.total(order)               # => 107.5

This tells us two useful facts about parameter defaults: first, they are evaluated dynamically, every time they are needed. Unlike the default tax rate constant we used before, the result of this default expression is going to be different depending on how the method is invoked. So we can see that Ruby is waiting until the method is actually called before evaluating that default expression.

If you are coming to Ruby from Python, this may be surprising. In Python parameter defaults are evaluated only once, at class definition time.

The second fact we learn from this code is that parameter default expressions can reference the value of arguments from earlier in the parameter list. In this case, we use the value of the first argument, order, in looking up the default for the second parameter.

We also see that it is possible to use arbitrary Ruby expressions in a parameter default. However, just because we can, doesn't mean we should. Complex expressions in parameter defaults are difficult to understand. I prefer to encapsulate them in helper methods.

Let's go ahead and do that now. We'll make a new, private method called #tax_rate_for_order. We'll copy the tax rate lookup code into this method. Then we'll update the tax_rate parameter in the #total method to call this new helper.

We can test this, and see that it still works.

Address = Struct.new(:street, :city, :state)

Order = Struct.new(:subtotal, :address)

class SaleCalculator
  TAX_RATES = {
    "PA" => 0.06,
    "CA" => 0.075
    # ...
  }

  def total(order, tax_rate = tax_rate_for_order(order))
    order.subtotal + order.subtotal * tax_rate
  end

  private

  def tax_rate_for_order(order)
    TAX_RATES[order.address.state]
  end
end

order = Order.new(100.0, Address.new("123 Main St.", "Anytown", "CA"))

calc = SaleCalculator.new

calc.total(order)               # => 107.5

Once again, this gives us some new insight into how parameter defaults work. Consider what we've done here: we've called a private method on the current object inside the default expression. What this tells us is that default parameter expressions are treated exactly as if they were being executed inside the body of the method that it precedes. Pretty much any code that would be valid in the body of the method is also valid in a parameter default.

And I do mean any code. To explore the implications of this, let's consider a different example method. This is a helper method like one we might find in any web application framework. It's called #content_tag, and we give it two arguments: the name of an HTML tag, and the string content which should be enclosed by that tag.

We can call it like so, passing a p tag and a short message.

def content_tag(tagname, content)
  "<#{tagname}>#{content}</#{tagname}>"
end

content_tag("p", "hello, world")
# => "<p>hello, world</p>"

With helpers such as this one, we often find it convenient to have an alternative form where we provide the contents of the tag in a block, instead of as an argument. This lets us cleanly write multi-line code inside the content block, and ensures that the content code will only be evaluated if and when it is needed.

We can make this form possible by making the content parameter default to nil. Then inside the method we can check to see if content has the special flag value of nil. If so, we fetch the real content by yielding to the method's block instead.

def content_tag(tagname, content = nil)
  content ||= yield
  "<#{tagname}>#{content}</#{tagname}>"
end

result = content_tag("p") {
  "hello there..." +
  "Today's lucky number is #{rand(100)}"
}
result
# => "<p>hello there\nToday's lucky number is 84</p>"

This works fine. But it's actually more code than we needed to write. Can you guess why?

The answer is that, since any code we can write in the method body is also valid in parameter defaults, we don't need the nil flag value at all. We can simply default the content parameter to equal the result of yield. If the content argument is not supplied, Ruby will evaluate the default expression, causing the passed block to be executed and its return value to be assigned to the argument.

def content_tag(tagname, content = yield)
  "<#{tagname}>#{content}</#{tagname}>"
end

result = content_tag("p") {
  "hello there... " +
  "Today's lucky number is #{rand(100)}"
}
result
# => "<p>hello there\nToday's lucky number is 51</p>"

So to sum up: Ruby parameter defaults are dynamically evaluated at method invocation time. They are evaluated as if they were part of the body of the method. They have access to private object members, as well as the values of arguments that come earlier in the method signature. And they can contain pretty much any expression which is valid inside a method.

Happy hacking!

Responses