Programmers Stack Exchange is a question and answer site for professional programmers interested in conceptual questions about software development. Join them; it only takes a minute:

Sign up
Here's how it works:
  1. Anybody can ask a question
  2. Anybody can answer
  3. The best answers are voted up and rise to the top

i'm using Ruby 1.9.3

I figured out that you can use an array, or a hash as hash key in ruby:

h = Hash.new

h[Array.new] = "Why?"
h[Array.new] # Output: "Why?"

h[Hash.new] = "It doesn't make sense"
h[Hash.new] # Output: "It doesn't make sense"

But an object works differently...

h[Object.new] = "LOL"
h[Object.new] # Output: "nil"

But this one works as expected:

o = Object.new

h[o] = "LMAO"
h[o] # Output: "LMAO"

Tried this:

o = Object.new           # Output: #<Object:0x2c78c10>
h["#<Object:0x2c78c10>"] # Output: nil

Tried it in Python and PHP and it throws an error.

I'm just curious about how it works, and why would you want to use an array, or a hash as hash key in ruby?

Thanks.

share|improve this question
up vote 4 down vote accepted

Why wouldn't you? You can use any object as a Hash key in Ruby. (Well, any object that responds to hash and eql?, but since there are definitions of those in Object, that's pretty much all objects.) It would be strange and inconsistent if you would arbitrarily exclude two classes from that.

share|improve this answer

Why limit the keys by a limited set of types? As long as it responds to hash, which is how hashes keep track of the key. Both Proc and lambda respond to hash and so can be used. As well as any other object, unless you explicitly remove that method from the object.

my_proc = Proc.new { "Hello World #{Time.now}!"}
my_hash = { my_proc => 'a proc', my_proc.call => "a proc that was called at #{Time.now}" , [3, 2, 1] => 'My 3, 2, 1 array'}
my_hash.default = "There is no key with this value"
puts my_hash[my_proc]
puts my_hash[my_proc.call].inspect
sleep(1)
puts my_hash[my_proc.call].inspect
puts my_hash[[3, 1, 2].sort.reverse]
puts my_hash
share|improve this answer

Well, you probably know this much already; but for starters, I'd say that's probably very bad practice. Still, it's an interesting functionality question, so I'll give it a shot...

From what I understand, Ruby maintains its empty arrays and hashes all as one singleton object, just as Nil is a single object in the entire program. If you declare 5 arrays with nothing in them, they're all the same reference - just like if you made 5 similar strings in C#, Javascript (and if I remember right, Ruby). They likely become seperate instances once values are added to them.

However, there are valid reasons to maintain Object.new's as seperate instances - the most common use I would know of is as multithreading tokens; times when you want to maintain something by hash map, but since it's so code-internal, a string isn't the most logical key.

The root of my answer, as I understand it, is that those classes work in kinda-exceptional ways within the language that you may not see with other types. I haven't used Ruby in a while, so I may need to get this verified by others.

share|improve this answer
    
So maybe arrays and hashes in Ruby are lazy loaded? – random_guy May 13 '13 at 21:23
1  
The reason that Object.new when called as a key doesn't match is because it is a new Object. It did not exist until you called it when you were trying to retrieve the key. This makes perfect sense, as you show when you store that new Object and then query the Hash for that value. – vgoff May 13 '13 at 22:06
    
The reason why they behave differently, is because they override hash and sql?, not because of some magical singleton object pooling caching thingy. I don't know of any Ruby implementation that does something even remotely similar to what you describe, and if there were, that would be a private internal implementation optimization detail that would not be allowed to change user-visible behavior. – Jörg W Mittag May 13 '13 at 22:37
    
There is nothing special about empty arrays/hashes. They're not "one singleton object": [].equal?([]) is false. What happens is they're compare by value: [].eql?([]) and [].hash == [].hash are true. Same would happen with h = {[1, 2] => 'x'}; h[[1, 2]] == 'x'. – Beni Cherniavsky-Paskin Apr 24 at 10:37

[How what you tried works first, why such keys are useful at the end.]

Behavior depends on the key, specifically what its .hash and .eql? methods do.
The 2 common strategies (appearing in many languages) are hashing by value and by identity. [Well, there is a third one: not allowing certain types as keys.]

An extremely familiar example is strings hashing by value:

pry(main)> a = 'text'
=> "text"
pry(main)> b = 'text'
=> "text"
pry(main)> a.equal? b  # Identity: not same object
=> false
pry(main)> a.eql? b    # But match for hash purposes
=> true
pry(main)> h = {a => 'foo'}
=> {"text"=>"foo"}
pry(main)> h[a]
=> "foo"
pry(main)> h[b]
=> "foo"

Right? It'd be very surprising and inconvenient if h['text'] wouldn't work simply because the 'text' you passed is not the very same 'text' object you inserted.

The same tends to be true of other types that "compare by value". When you look up h[['Sherlock', 'Holmes']], you probably care about the array's content, not its distinction from other arrays with same elements. E.g. in Ruby, arrays, hashes, sets define .hash and .eql? to hash by value. [See below for the "why?" part.]

Now what about custom objects? Well, you decide :-)
But the default behavior of Object, in Ruby as well as Python, Java and probably others is hashing by identity:

pry(main)> a = Object.new
=> #<Object:0x0055a2f2e803d8> 
pry(main)> b = Object.new
=> #<Object:0x0055a2f2387288>
pry(main)> h = {a => 'a', b => 'b'}
=> {#<Object:0x0055a2f2e803d8>=>"a", #<Object:0x0055a2f2387288>=>"b"}
pry(main)> h[a]
=> "a"
pry(main)> c = Object.new
=> #<Object:0x0055a2f214e520>
pry(main)> h[c]
=> nil

Same holds for subclasses of Object, unless you override the relevant methods (eql? and hash in Ruby). Why?

  • The concepts of "value" and "equality" vary, and can only be user-defined.
  • There may not be a concept of "value" at all! Let's say I have GUI Window objects, and want a hash mapping windows to their position. Are 2 different windows ever "equal"? No. All I can go by is "this window is here".
  • Many custom objects are mutable, so identity matters [see below].

All this adds up to identity being the only default hashing behavior that Object can provide with no explicit input on your part. (The alternative would be not allowing keys that didn't explicitly define how they hash at all.)


Notes on Mutability:

  1. Thinking in terms of "values" is easiest with immutable types — their identity never matters.
  2. Mutating an object you've used as a hash key is bad. It now sits in the wrong bucket, making the hash table internally inconsistent => at best, you'll not be able to lookup this entry, neither by original key nor by current value (I think Ruby sits here), at worst you could trigger implementation bugs and crash...

Both lead to a strong correlation: it's very common for immutable types to hash by value, and mutable by identity (that's safe, if hash function is only derived from identity, mutations don't matter).

So, how do you hash by an array value? Varies across languages...

  • In Ruby, you should ask even about strings, as they're mutable! Ruby played both sides here. You can mutate strings unless you .freeze them. (And there is a separate type — symbols — that are somewhat string-like but immutable, and even ensure there is at most one instance with a given value.) But mostly you do treat strings as values, and as we said you really want them to hash by value too. So they do :-).
    Plus Ruby special-cases strings to be cloned and frozen when used as keys, to keep you from trouble.

    Ruby arrays, hashes, sets hash by value too; it's your responsibility not to mutate them.

    • Python takes the stricter approach that you can't use mutable builtin types as keys. They don't (and can't) really enforce it for custom classes, but eg. Python arrays (list) override .__hash__() to raise an exception. Instead, Python provides a sister immutable tuple type, that hashes by value. Similarly, you can't have set keys but python provides frozenset...

    • Java collections took a deliberate decision to not have a combinatorial explosion of interfaces. All lists, mutable or not, hash by value. And you can use e.g. mutable ArrayList as hash keys. And it's your responsibility to not mutate it.


So back to Why would you use non-string keys, say arrays?

Frequently your data is naturally organized by several "keys". You can use h[[x, y, z]] as a "multi-dimesional" hash. You could also use nested "one-dimensional" hashes: {x => {y => {z => value}}}; sometimes one is more convenient, sometimes the other.

Consider graph edges. Let's say Magellan's expedition left us these 2 maps, to navigate the world's oceans:

a = {['Atlantic', 'Pacific'] => 'Straights of Magellan',
     ['Pacific', 'Indian'] => 'Indonesia',
     ['Indian', 'Atlantic'] => 'Cape of Good Hope'}
n = {'Atlantic' => {'Pacific' => 'Straights of Magellan'},
     'Pacific' => {'Indian' => 'Indonesia'},
     'Indian' => {'Atlantic' => 'Cape of Good Hope'}}
  • If you are in ocean1 and want to get to ocean2, you look up a[[ocean1, ocean2]], or n[ocean1][ocean2].
  • If you want to see all connections at once, you do a.keys. Nice!
    With the nested representation it's trickier.
  • But if you're in ocean1 and are asking "where could I go next?", the flat a representation is tricky, here n[ocean1] wins.

But now consider navigating east. Our graph is "undirected"; a passage from Atlantic to Pacific also works the other way around.

  • The n[from][to] access pattern is inherently "directed". To model it working both ways, we need 2 entries for every passage...
  • The 1-level a hash may be cleaner then. All we need is either normalizing order on every access (sorting the 2-element arrays), or keys that don't care about order:

    pry(main)> w = {Set.new(['Atlantic', 'Pacific']) => 'Straights of Magellan', pry(main)* Set.new(['Pacific', 'Indian']) => 'Indonesia',
    pry(main)* Set.new(['Indian', 'Atlantic']) => 'Cape of Good Hope'}
    => {#=>"Straights of Magellan", #=>"Indonesia", #=>"Cape of Good Hope"}

    pry(main)> w[Set.new(['Atlantic', 'Pacific'])] => "Straights of Magellan" pry(main)> w[Set.new(['Pacific', 'Atlantic'])] => "Straights of Magellan"

See? Fancy types as keys are fun!

A less abstract example: Fitting an existing API. If I have words and decide to segment them by length, I do:

pry(main)> ['foo', 'bar', 'baz', 'quux'].group_by { |word| word.length }
=> {3=>["foo", "bar", "baz"], 
    4=>["quux"]}

Now let's say I want to group words that have same length and same first letter.

pry(main)> ['foo', 'bar', 'baz', 'quux'].group_by { |word| [word[0], word.length] }
=> {["f", 3]=>["foo"], 
    ["b", 3]=>["bar", "baz"], 
    ["q", 4]=>["quux"]}

Easy and natural! I used existing logic, just with more complicated keys! How could you write it in a language that wouldn't let you compose keys this way?

share|improve this answer

It's a bit unusual, but why not? Let's say I need to lookup objects based on two integer values, then it would be totally obvious to have a hash that uses an array with two integers as the key.

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.