Search code examples
rubyargumentsdice

Manipulating Ruby class default values


When there is more than one default value, how could I change only the second initialization variable without also calling the first?

For example, a Ruby class is created to return the value akin to the roll of a single die with default values for a six sided die ranging from 1 to 6:

class Die

  def initialize(min=1, max=6)
    @min       = min
    @max       = max
  end

  def roll
    rand(@min..@max)
  end
end

If I wanted instead to use this code to simulate the return from rolling a 20 sided die, I could write the following:

p Die.new(min=1, max=20).roll

...but is there a way to argue only the second (max) value?

Of note - and this is where I am confused (I don't fully understand Ruby class attributes and variable scopes) - if I invoke:

p Die.new(max=20).roll

... I get nil printed. ?. (I understand that this is because rand(20..6) returns nil, but I thought that max= would retain the default min value for the first argument - instead max=20 gets ingested as the integer 20 binding to the min=... This seems weird to me.)

I suppose I could re-work the Die class to take a default value of the number of sides and also set the min (or max) value relative to the number of sides, but this is beside the point of my main question: How to override only the second default value without explicitly writing the first as well...

Presuming that most dice would normally have a minimum value of 1, I realize that I could reverse the order of min and max like so:

class Die2

  def initialize(max=6, min=1)
    @max       = max
    @min       = min
  end

  def roll
    rand(@min..@max)
  end  
end

...and then invoke whatever maximum number of sides like so:

p Die2.new(20).roll

...but given the syntax of class Die (and my inclination to write the minimum before the maximum) is there a way to only enter an argument for the second value? Or, perhaps I am approaching Ruby classes poorly? Any help or guidance is appreciated - thanks!


Solution

  • If you write

    class Die
      def initialize(min=1, max=6)
        @min, @max = min, max
      end
    end
    

    and create a new instance by passing a single argument, such as:

    die = Die.new(3)
      #=> #<Die:0x007fcc6902a700 @min=3, @max=6>
    

    we can see from the return value that the argument 3 has been assigned to @min and @max gets its default value. In short, to pass a value to @max you must also pass one to @min (unless, of course, you reverse the order of the arguments).

    You can do what you want by using named arguments (or named parameters), introduced in Ruby v2.0.

    class Die
      def initialize(min: 1, max: 6)
        @min, @max = min, max
      end
    end
    
    die = Die.new(max: 3)
      #=> #<Die:0x007fcc698ccc00 @min=1, @max=3>
    

    (or die = Die.new(:max=>3). As you see, @min equals its default value and @max equals the argument that is passed, 3.

    Default values were required for keyword arguments in Ruby v2.0, but v2.1 extended their functionality to permit required named arguments as well. See, for example, this article.

    Lastly, consider the following two cases (the second being the more interesting).

    class Die
      def initialize(min=1, max: 6)
        @min, @max = min, max
      end
    end
    die = Die.new(max: 3)
      #=> #<Die:0x007fcc69954448 @min=1, @max=3>
    
    class Die
      def initialize(min, max: 6)
        @min, @max = min, max
      end
    end
    die = Die.new(max: 3)
      #=> #<Die:0x007fa01b900930 @min={:max=>3}, @max=6>