Can Ruby Do This?

Discussion in 'Ruby' started by Mr Bubb, Feb 4, 2010.

  1. Mr Bubb

    Mr Bubb Guest

    In Perl, you can create a hash of arbitrary depth like so:

    $hash->{foo}->{bar}->{baz} = 123;

    I've tried this in ruby:

    hash[:foo][:bar][:baz] = 123

    but I get nil.= errors

    I could of course do a check for null at each level, but oy, that's so
    cumbersome. Is there a ruby way?

    Thanks!
     
    Mr Bubb, Feb 4, 2010
    #1
    1. Advertisements

  2. Mr Bubb

    Daniel N Guest

    [Note: parts of this message were removed to make it a legal post.]

    Interesting little problem.

    Just as a quick belt out, you could do something like this:
    => {:foo=>{:bar=>{:baz=>"barry"}}}

    HTH
    Daniel

     
    Daniel N, Feb 4, 2010
    #2
    1. Advertisements

  3. [Note: parts of this message were removed to make it a legal post.]

    $ irb
    irb(main):001:0> hash[:foo][:bar][:baz] = 123
    NoMethodError: undefined method `[]' for nil:NilClass
    from (irb):1
    from /usr/bin/irb:12:in `<main>'
    irb(main):002:0> hash = { :foo => { :bar => { :baz => 123} } }
    => {:foo=>{:bar=>{:baz=>123}}}
    irb(main):003:0> hash[:foo][:bar][:baz]
    => 123

    I'm not sure if there's a way that's a little less verbose...
     
    Steve Klabnik, Feb 4, 2010
    #3
  4. Mr Bubb

    Gary Wright Guest

    It is a bit tricky because you have to specify a default
    proc that refers to itself (to get the infinite depth):
    ?> top[0][1][2][3] = 4
    => 4?> p top
    {0=>{1=>{2=>{3=>4}}}}
    => nil



    Gary Wright
     
    Gary Wright, Feb 5, 2010
    #4
  5. Mr Bubb

    Kirk Haines Guest

    Yes.

    You need to understand Ruby a little better, though.

    h = {}
    p h[:a]

    => nil

    That is, if you have a hash, and you reference an element in the hash
    that does not exist, the default behavior is for the hash to return
    nil.

    So, if you do this:

    h = {}
    p h[:a][:b]

    You will get this:

    NoMethodError: undefined method `[]' for nil:NilClass

    This is because h[:a] returns a nil, as you already discovered, and
    NilClass does not define an [] method. Since, in Ruby, you are just
    chaining method calls, your h[:a][:b][:c] style syntax can only work
    if the return element from any given level is an object that provides
    an [] method.

    Now, consider this example:

    h = Hash.new {|h,k| h[k] = {}}
    p h[:a]

    => {}

    Ah! Now we're on to something.

    h = Hash.new {|h,k| h[k] = {}}
    p h[:a][:b]

    => nil

    Hmmm.

    h = Hash.new {|h,k| h[k] = {}}
    p h[:a][:b][:c]

    => NoMethodError: undefined method `[]' for nil:NilClass

    Oh, dang. Do you see what's happening here? Hash's new method can
    take a block. If it is given a block, then it will call that block
    with arguments of the hash, itself, and the key that was given, if
    it's asked to lookup a key that does not exist in the hash. So, h[:a]
    ran that block, creating a value at :a => {}. h[:a][:b], though, gets
    us back to the default Hash behavior. Still, you see one solution
    here, I bet:

    h = Hash.new {|h,k| h[k] = Hash.new {|h2,k2| h2[k2] = {}}}
    h[:a][:b][:c] = 123
    p h[:a][:b][:c]

    => 123

    That works. It's kind of hard to easily read, though. So, you could
    rewrite it like this:

    h = Hash.new do |h1,k1|
    h1[k1] = Hash.new do |h2,k2|
    h2[k2] = {}
    end
    end

    That, at least, lets the eye follow along a little better, but it's
    still limited because you must decide what depth of auto
    initialization you are going to support ahead of time. If you try
    something like h[:a][:b][:c][:d][:e] you will again get that
    unpleasant NoMethodError.

    However, step back just a moment. The hash initializer accepts a
    block. And, if you look at the Hash methods, you'll see a
    Hash#default_proc.

    It does just what it sounds like it does -- it returns the default
    proc, which is what the block is turned into when the new method
    captures it and stores it.

    So, all you need to do is this:

    h = Hash.new {|hash,key| hash[key] = Hash.new(&hash.default_proc)}
    h[:a][:b][:c][:d][:e] = 12345
    p h[:a][:b][:c][:d][:e]

    => 12345

    Hopefully this helps you follow what's happening with Ruby's method
    calls and Hash initializers.


    Kirk Haines
     
    Kirk Haines, Feb 5, 2010
    #5
  6. Sure, but there's probably also a better way that doesn't involve so
    much Hash nesting. Can you explain what you're using this for?
    Best,
    -- 
    Marnen Laibow-Koser
    http://www.marnen.org
     
    Marnen Laibow-Koser, Feb 5, 2010
    #6
  7. Mr Bubb

    Mr Bubb Guest

    Well, in my job (bioinformatics), I only use code like this about a
    hundred times a day. I've been doing this job for 15 years, and
    actually, there is no better way. You must not need to quickly create
    data structures from flat files, I guess.

    Jim
     
    Mr Bubb, Feb 5, 2010
    #7
  8. If you're so sure of that, why are you asking for help? There may be no
    better way in Perl, but Ruby is not Perl. :)
    Instead of assuming what my needs are, let's focus on yours. Nested
    hashes *may* be the best solution in Ruby, but if you provide more
    information about your use case, we can give you better help.
    Best,
    -- 
    Marnen Laibow-Koser
    http://www.marnen.org
     
    Marnen Laibow-Koser, Feb 5, 2010
    #8
  9. Mr Bubb

    Mr Bubb Guest

    Is there a hidden camera, or something? It's not legit to say "I'm a
    ruby newbie, is there a way to do x in ruby?" If I had said, hey perl
    has one line comments, how do you do one line comments in ruby, would
    you answer, well, why do you need one line comments? Jeez.
    This is like the old Bostonian joke:

    Question: "Can you tell me how to get to Roxbury?"
    Answer: "Whaddaya wanna go there for?"

    I guess what I'm saying is, thanks anyway.
     
    Mr Bubb, Feb 5, 2010
    #9
  10. Mr Bubb

    Walton Hoops Guest

    No I wouldn't, if on the other hand you came to me and asked "How do you
    do write a 'for' loop in Ruby?"
    I would indeed ask "why do you need a 'for' loop?" Why ask? Because
    while writing something akin to a 'for'
    is *possible* in Ruby, Ruby has other methods of achieving the same
    result which are preferred. Thus,
    seeing as your original question has been dealt with by others, it makes
    perfect sense to ask _why_
    you would want to do something that just doesn't seem like the best
    _Ruby_ solution in most cases.
     
    Walton Hoops, Feb 5, 2010
    #10
  11. Well said. If you want to get the most out of a language, it's worth
    investigating its idioms. Sometimes they are not worth copying, but
    more often they are. You can write Fortran in Ruby, or Lisp in C++, but
    usually you don't want to.

    Best,
    -- 
    Marnen Laibow-Koser
    http://www.marnen.org
     
    Marnen Laibow-Koser, Feb 5, 2010
    #11
  12. Mr Bubb

    Kirk Haines Guest

    Let me know if my detailed explanation didn't make sense. The long
    and the short of it is this:

    auto_nesting_hash = Hash.new {|hash,key| hash[key] =
    Hash.new(&hash.default_proc)}

    It may well be the best way to do whatever it is you are doing. What,
    I think, Marnen was trying to communicate, though, is that there may
    be an alternative that performs better in Ruby which would become more
    obvious if we understood the actual use a little better.

    Just as a quick example, there was a thread last week where someone
    was asking about while loop performance, and wondering why a while
    loop in Ruby isn't so fast as he expected.

    i = 0
    while i < 10000000
    i += 1
    end

    Perfectly legitimate Ruby, and comparable to similar looping
    structures in other languages. But, the real answer there is that
    there's another way to do it in Ruby that's substantially faster.

    0.upto(10000000) {|i| #do whatever you need to do with i }

    So, if the auto nesting hashes work for you, that's great. If you
    want to bend people's minds towards perhaps suggesting a better
    alternative, though, provide more details. Someone may surprise you
    with something unexpected, and superior.


    Kirk Haines
     
    Kirk Haines, Feb 5, 2010
    #12
  13. IMHO this is the most concise and elegant way to do it in Ruby! I am
    surprised you are the only advocate of this.
    This one does one iteration more than the original code if I'm not
    mistaken. There's also

    10000000.times {|i| #do whatever you need to do with i }
    Just as a vague idea: a similar thing could be created with OpenStruct
    accesses - saves you a lot of typing of []. Then you could do
    hash.foo.bar.baz = 123

    class AutoNest
    def method_missing(s,*a,&b)
    case s
    when /\A(\w+)=\z/
    class <<self;self;end.class_eval { attr_accessor $1 }
    send(s,*a)
    when /\A\w+\z/
    class <<self;self;end.class_eval { attr_accessor s }
    x = self.class.new
    send("#{s}=",x)
    x
    else
    super
    end
    end
    end

    hash = AutoNest.new
    hash.foo.bar.baz = 123
    p hash, hash.foo.bar.baz

    I'd probably stick with your solution though.

    Kind regards

    robert
     
    Robert Klemme, Feb 5, 2010
    #13
    1. Advertisements

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments (here). After that, you can post your question and our members will help you out.