In Progress
Unit 1, Lesson 1
In Progress

# Keyword Sugar

Video transcript & code

I'm a little short on time this week, so I thought I'd just share a quick little tip with you.

Let's haul out everyone's favorite example class, a `Point` object. Let's say this object is part of a library that's already in wide use. Up until now, everyone who wanted a point object has passed in two positional arguments: one for the X coordinate, and one for the Y coordinate.

``````class Point
def initialize(x, y)
@x, @y = x, y
end

def inspect
"Point(#{@x}x#{@y})"
end
end

Point.new(42, 23)               # => Point(42x23)
``````

Lately we've been playing with the features in Ruby 2.0, and we thought it might be nice to release a version of this library which takes keyword arguments for the X and Y coordinates. This would make it explicitly clear which argument is which when creating a new `Point`.

The drawback is that this will break all the existing code that expects a `Point` to accept positional arguments.

``````class Point
def initialize(x:, y:)
@x, @y = x, y
end

def inspect
"Point(#{@x}x#{@y})"
end
end

Point.new(x: 2, y: 3)           # => Point(2x3)
Point.new(42, 23)               # =>

# ~> ArgumentError
# ~> missing keywords: x, y
# ~>
# ~> xmptmp-in22392_Yv.rb:12:in `new'
# ~> xmptmp-in22392_Yv.rb:12:in `<main>'
``````

Fortunately, there is an easy way to have our cake and eat it to. One of the cool features of Ruby arguments is that we can actually reference earlier argument values in the defaults for later parameters.

This is easier to show than to explain. We start out with some positional arguments for X and Y. We prefix them with underscores, because we're going to need the parameter names "x" and "y" for keyword We also give them each a default value of `nil`.

Next we add keyword arguments for `x` and `y`. This time, we used the un-prefixed names. For the default values of these keywords, we reference the value of the earlier positional arguments. This means that if positional arguments are supplied, they will also become the value of the un-specified keyword arguments. But if keyword arguments are used, it will override the default `nil` values from the positional parameters.

The rest of the code stays the same. We can now initialize `Point` objects using either positional argumentsâ€¦ or keywords. We've remained backward compatible with all the existing clients of our code, while also providing a new, more explicit interface.

``````class Point
def initialize(_x=nil, _y=nil, x: _x, y: _y)
@x, @y = x, y
end

def inspect
"Point(#{@x}x#{@y})"
end
end

Point.new(42, 23)               # => Point(42x23)
Point.new(x: 2, y: 3)           # => Point(2x3)
``````

There's only one drawback to our new version of the `Point` class. It's now possible to omit arguments entirely, and Ruby won't catch this case for us.

``````class Point
def initialize(_x=nil, _y=nil, x: _x, y: _y)
@x, @y = x, y
end

def inspect
"Point(#{@x}x#{@y})"
end
end

Point.new                       # => Point(x)
``````

If we want to ensure that a `Point` will always have non-nil X and Y coordinates, we have to add an explicit precondition for it. This code will raise an error if either X or Y are missing.

``````class Point
def initialize(_x=nil, _y=nil, x: _x, y: _y)
x && y or raise ArgumentError, "Must supply X and Y coordinates"
@x, @y = x, y
end

def inspect
"Point(#{@x}x#{@y})"
end
end

Point.new                       # =>

# ~> ArgumentError
# ~> Must supply X and Y coordinates
# ~>
# ~> xmptmp-in22392lSL.rb:3:in `initialize'
# ~> xmptmp-in22392lSL.rb:12:in `new'
# ~> xmptmp-in22392lSL.rb:12:in `<main>'
``````

And that's it for today: an easy way to provide both positional and keyword arguments for the same parameters. Happy hacking!