Why would you want to use an array, or hash as hash key in ruby?

i’m using Ruby 1.9.3

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>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"
</code>
<code>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" </code>
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…

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>h[Object.new] = "LOL"
h[Object.new] # Output: "nil"
</code>
<code>h[Object.new] = "LOL" h[Object.new] # Output: "nil" </code>
h[Object.new] = "LOL"
h[Object.new] # Output: "nil"

But this one works as expected:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>o = Object.new
h[o] = "LMAO"
h[o] # Output: "LMAO"
</code>
<code>o = Object.new h[o] = "LMAO" h[o] # Output: "LMAO" </code>
o = Object.new

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

Tried this:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>o = Object.new # Output: #<Object:0x2c78c10>
h["#<Object:0x2c78c10>"] # Output: nil
</code>
<code>o = Object.new # Output: #<Object:0x2c78c10> h["#<Object:0x2c78c10>"] # Output: nil </code>
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.

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.

[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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>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"
</code>
<code>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" </code>
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>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
</code>
<code>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 </code>
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>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'}}
</code>
<code>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'}} </code>
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:

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    <code>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'}
    => {#<Set: {"Atlantic", "Pacific"}>=>"Straights of Magellan",
    #<Set: {"Pacific", "Indian"}>=>"Indonesia",
    #<Set: {"Indian", "Atlantic"}>=>"Cape of Good Hope"}
    pry(main)> w[Set.new(['Atlantic', 'Pacific'])]
    => "Straights of Magellan"
    pry(main)> w[Set.new(['Pacific', 'Atlantic'])]
    => "Straights of Magellan"
    </code>
    <code>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'} => {#<Set: {"Atlantic", "Pacific"}>=>"Straights of Magellan", #<Set: {"Pacific", "Indian"}>=>"Indonesia", #<Set: {"Indian", "Atlantic"}>=>"Cape of Good Hope"} pry(main)> w[Set.new(['Atlantic', 'Pacific'])] => "Straights of Magellan" pry(main)> w[Set.new(['Pacific', 'Atlantic'])] => "Straights of Magellan" </code>
    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'}  
    => {#<Set: {"Atlantic", "Pacific"}>=>"Straights of Magellan",
     #<Set: {"Pacific", "Indian"}>=>"Indonesia",
     #<Set: {"Indian", "Atlantic"}>=>"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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>pry(main)> ['foo', 'bar', 'baz', 'quux'].group_by { |word| word.length }
=> {3=>["foo", "bar", "baz"],
4=>["quux"]}
</code>
<code>pry(main)> ['foo', 'bar', 'baz', 'quux'].group_by { |word| word.length } => {3=>["foo", "bar", "baz"], 4=>["quux"]} </code>
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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>pry(main)> ['foo', 'bar', 'baz', 'quux'].group_by { |word| [word[0], word.length] }
=> {["f", 3]=>["foo"],
["b", 3]=>["bar", "baz"],
["q", 4]=>["quux"]}
</code>
<code>pry(main)> ['foo', 'bar', 'baz', 'quux'].group_by { |word| [word[0], word.length] } => {["f", 3]=>["foo"], ["b", 3]=>["bar", "baz"], ["q", 4]=>["quux"]} </code>
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?

1

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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>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
</code>
<code>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 </code>
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

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.

4

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.

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật