The Use Case
Earlier I was writing a test to ensure that a Hash contains a specific key and value.
1
2
3
it "sets the anonymous key to true" do
expect(properties).to include { a: true }
end
It then dawned on me that this kind of comparison, while possible in the RSpec DSL with its built-in matchers1 was actually impossible in raw Ruby.
Take a Hash:
1
x = { a: true, b: false }
Now take another Hash:
1
y = { a: true }
You want to check whether y
is included within x
or not. So what’s the first
thing that comes to mind? For me, it’s String#include?
, which works like this:
1
2
"Olivier".include?("Oli")
# => true
This is probably one of the most famous whoa-worthy Ruby demonstrations when
someone wants to show the object-oriented power of the language. So naturally,
when I’m dealing the x
and y
hashes above, I reach for include?
:
1
2
x.include?(y)
# => false
Well, obviously that’s not (pardon incoming pun) true! Let’s look at the Ruby API
docs to see how Hash#include?
is defined:
include?(key) → true or false Returns true if the given key is present in hsh.
1
2
3
h = { "a" => 100, "b" => 200 }
h.has_key?("a") #=> true
h.has_key?("z") #=> false
If this leaves you puzzled, I can empathize. The fact that the example isn’t
even using include?
shows that it’s used as a method alias for Hash#has_key?
which does have a good name. Hash#include?
however, is very poorly named because
it breaks a reasonable expectation.
You cannot say that Hash#include?
is checking whether something “is included
in the Hash instance” because this method only checks keys. If it were checking
both the keys and the values then perhaps this would make sense.
The Basic Implementation
So here’s my proposition for a reasonable and simple re-implementation2 of Hash#include?
:
1
2
3
4
5
class Hash
def include?(other)
self.merge(other) == self
end
end
It seems incredibly simple but by that token, why not offer such a useful method to Ruby developers instead? It seems silly to monkey patch the Hash class for something like this when it could benefit all Ruby developers to have in Ruby core.
Demonstration
And here’s a demonstration of it in use:
1
2
3
4
5
6
7
8
9
10
11
{ a: true, b: false }.include?({ a: true})
# => true
{ a: true, b: false }.include?({ b: false})
# => true
{ a: true, b: false }.include?({ a: false})
# => false
{ a: true, b: false }.include?({ c: true})
# => false
I’m was thinking about submitting this implementation to the Ruby core team to see if
they would consider including it in a major version of the language. Obviously,
this would be an API breaking change since all existing programs using the current
implementation of Hash.include?
would suddenly stop working properly.
A way to alleviate that concern would be to use another method name than include
which I don’t think is a good idea. That said, I could be convinced that
Hash#contain?
could be a suitable alternative. This name would prevent an API
breaking change and could potentially allow this method to be shipped with Ruby 2.2.
What surprises me is that the very
definition of the equivalent Array#include?
is hinting at what Hash.include?
should be:
Returns true if the given object is present in self (that is, if any element == object), otherwise returns false.
And with that, I rest my case.
I’d like to thank Jim Gay, Pat Shaughnessy and Terence Lee for their feedback and support. I’ll submitting a feature request for Ruby 2.2 shortly unless someone can find a reason for me not to.
Update 1 (August 28th, 2014)
Some time has passed since this post and shortly after Nobu — the amazing Ruby core team member known as “Patch Monster” — created a patch that actually implements this proposal, albeit with name I find confusing.
After talking with some Ruby developers, including Adam Rensel who works with
me at Code School and also often has the need to check whether a Hash is
partially or completely included in an other Hash, I settled on a better
name for this method: Hash#contain?(Hash)
I know I said I would be submitting a feature suggestion to Ruby Core and sadly I haven’t done that yet because I wanted to make sure that this wasn’t frivolous. But I swear I’ll do it before RubyConf 2014.
To be honest I wish we could extend the capabilities of Hash#include?
because its current implementation
seems woefully simplistic. It’s a mere alias to Hash#has_key?
which is
inconsistent with the meaning of “include” in my book. I believe that if
Hash#include?
(a real one, not an alias) receives a Hash, it should do
actually compare the two hashes (both keys and values). If it receives
a String or a Symbol, it should defer to Hash#has_key?
.
Update 2 (March 19th, 2015)
After a busy end of 2014 I finally found the time to submit a proper feature proposal to bugs.ruby-lang.org.
The feature is being debated with fellow Rubyists at the moment.
Update 3 (November 9th, 2015)
After some stagnation, the feature proposal was discussed in two separate Ruby
core team developer meetings. First on May 14th, 2015
and then this morning for the November meeting. Matz said he favored a comparison operator (<=
or >=
) because
it’s less ambiguous than contain?
.
This means that the syntax I originally proposed would evolve to something like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{ a: 1, b: 2 } >= { b: 2 }
=> true
{ b: 2 } <= { a: 1, b: 2 }
=> true
# also
{ b: 2 } >= { b: 2 }
=> true
{ a: 1 } >= { b: 2 }
=> false
{ b: 2 } <= { b: 2 }
=> true
{ b: 2 } <= { a: 1 }
=> false
I have to admit I was originally surprised by Matz’ proposal (it was early) but I’ve since then been convinced by its versatility and simplicity. I really hope it can be included in Ruby 2.3 for the December 25th release. That would be quite the Chritmas present. :-)
-
You can see how this include matcher is implemented in the rspec-expectations source. ↩
-
I’m using
Hash#merge
for convenience and as a hack. I don’t know if it’s appropriate or performant enough. ↩