C2a5bc0633a2eef9274744bd273063a4

Yesterday i wrote this neat little hash so I don't can do something like myhash['a']['b']['c'] || default_vlaue. (Even when there is no h['a'] in the first place).

I thought it would be great example for refactoring. It could be made into one class, or just clean up/simplify the logic.

I would really like to see what you do to tihs code!

so whats so magical about this Hash?

h = Lang::Hash.new()
h.get['some']['long']['nonexistant']['nesting'].or 'mydefaultvalue' #gives [mydefaultvalue]

with ordinary hash, same code wold look like this:

h = Hash.new()
h['some'] ||= {}
h['some']['long'] ||= {}
h['some']['long']['nonexistant'] ||= {}
h['some']['long']['nonexistant']['nesting'] || 'mydefaultvalue' #gives [mydefaultvalue]

setting a value to nonexistant hash is done like this:
h.get['some']['long']['nonexistant']['nesting'] = 'actual value'
h.get['some']['long']['nonexistant']['nesting'].or 'mydefaultvalue' #gives [actualvalue]
module Lang
    class Hash < ::Hash
      def get
        HashValue.new(self)
      end

      def [] k
        unless keys.include? k
          self[k] = Hash.new
        end
        super
      end

      def or(other)
        if empty?
          other
        else
          self
        end
      end
    end

    class HashValue < Hash
      attr_accessor :value

      def initialize value
        @value = value
      end

      def [] k
        v = value[k]
#        if v.is_a? Hash
#          v
#        else
          HashValue.new(v)
#        end
      end

      def []= k, v
        value[k] = v
      end

      def or other
        #p @value
        if @value.is_a?(Hash) && !@value.empty?
          #p [:a, @value]
          @value
        elsif !@value.is_a?(Hash) && @value
          #p [:b, @value]
          @value
        else
          #p :c
          other
        end
      end
    end    
  end
class HashTest < Test::Unit::TestCase
      include Lang

      def test_hash_nil_value_or_returns_other
        h = Hash.new
        val = h['nonexistant']
        assert_equal 'val-was-false', (val.or 'val-was-false')
      end

      def test_hash_value_or_returns_value
        h = Hash.new
        h['a'] = 'a value'
        val = h.get['a']
        p val
        assert_equal 'a value', (val.or 'b value')
      end

      def test_hash_value_can_set_unset_stuff
        h = Hash.new
        h.get['a'] = 'value'
        assert_equal 'value', h.get['a'].or('nil get')
        assert_equal 'value', h['a']
      end

      def test_hash_value_can_set_unset_stuff_nested
        h = Hash.new
        h.get['a']['b'] = 'value'
        assert_equal 'value', h['a']['b']
        assert_equal 'value', h.get['a']['b'].or('nil get')
      end

      def test_hash_value_can_set_unset_stuff_more_nested
        h = Hash.new
        h.get['a']['b']['c'] = 'value'
        assert_equal 'value', h['a']['b']['c']
        assert_equal 'value', h.get['a']['b']['c'].or('nil get')
      end
    end

Refactorings

No refactoring yet !

C1f7bc8064161e7408ef62d97bb636ac

Mortice

October 8, 2009, October 08, 2009 08:54, permalink

No rating. Login to rate!

The following will get you to 3 levels of nesting but not 4. I feel like there's probably a neat trick to get to N levels using a similar technique to the code below, but I can't quite see it yet...

class NilClass
  def [](any)
    nil
  end
end

def create_hash
  Hash.new do |hash, key|
    hash[key] = Hash.new({})
  end
end

h = create_hash
h['a']['b']['c'] or "default" # => "default"

h = create_hash
h['a']['b']['c'] = "hello"
h['a']['b']['c'] or "default" # => "hello"
C1f7bc8064161e7408ef62d97bb636ac

Mortice

October 8, 2009, October 08, 2009 09:03, permalink

No rating. Login to rate!

Always the way isn't it? I've found the neat trick.

#change create_hash to...

def create_hash
  Hash.new do |hash, key|
    hash[key] = hash.dup
  end
end

#and you get as many levels of nesting as you like.
C1f7bc8064161e7408ef62d97bb636ac

Mortice

October 8, 2009, October 08, 2009 09:23, permalink

No rating. Login to rate!

Bah, using Hash#dup messes up the default value. To get that back, you need the below. Otherwise you always get an empty hash.

Apologies for the triple refactoring - only just managed to get logged in.

class Hash
  def or(val)
    val if self.empty?
  end
end

# now the below works as expected

h = Hash.new {|h,k| h[k] = h.dup}
h['a']['b']['c'].or "default"

# but this returns h.dup...

h['a']['b']['c'] or "default"
B63c2d00125474973e2c1919f610fd92

rainchen

October 8, 2009, October 08, 2009 10:40, permalink

No rating. Login to rate!

Mortice:
No need to extend Hash, use the "first" method is OK:

irb
irb(main):001:0> h = Hash.new {|h,k| h[k] = h.dup}
=> {}
irb(main):002:0> h['a']['b']['c'].first || "default"
=> "default"
irb(main):003:0> h['a']['b']['d'] = 'hi'
=> "hi"
irb(main):004:0> h['a']['b']['d'].first || "default"
=> "hi"
A8d3f35baafdaea851914b17dae9e1fc

Adam

October 9, 2009, October 09, 2009 00:57, permalink

No rating. Login to rate!

@Mortice dup-ing the hash copies the values that already exist in the hash leading to values that should not exist.

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

Adam

October 9, 2009, October 09, 2009 04:26, permalink

No rating. Login to rate!

And a convoluted solution that supports the || operator.

class MagicHash < Hash
  module NilExtension
    def __set_magic_hash__(hash, key)
      @hash = hash[key] = MagicHash.new
    end

    def [](key)
      if @hash
        @hash = @hash[key] = MagicHash.new
        nil
      else
        raise NoMethodError, "undefined method `[]' for nil:NilClass"      
      end
    end

    def []=(key, value)
      if @hash
        @hash[key] = value 
      else
        raise NoMethodError, "undefined method `[]=' for nil:NilClass"
      end
    end
  end
  
  def [](key)
    nil.__set_magic_hash__(self, key)
    nil
  end
  
  NilClass.send(:include, NilExtension)
end
hash = MagicHash.new

hash[1][2][3] = true
=> {1=>{2=>{3=>true}}}

hash[1][2][3] || 'Foo'
=> true

hash[0][1][2] || 'Foo'
=> 'Foo'
D41d8cd98f00b204e9800998ecf8427e

mainej

October 9, 2009, October 09, 2009 05:35, permalink

No rating. Login to rate!

Here's a little refactoring that passes all the original tests. It depends on the values responding to `empty?`, so it works with strings and hashes, but not much else.

module Lang
  class Hash < ::Hash
    def initialize
      super { |h,k| h[k] = Lang::Hash.new }
    end

    def get
      self
    end

    def [](k)
      v = super
      def v.or(other)
        empty? ? other : self
      end
      v
    end
  end
end
D41d8cd98f00b204e9800998ecf8427e

mainej

October 9, 2009, October 09, 2009 05:40, permalink

No rating. Login to rate!

Here's another refactoring: `get` has become trivial, so remove it. Removing a method changes the class' interface, but in a good way.

module Lang
  class Hash < ::Hash
    def initialize
      super { |h,k| h[k] = Lang::Hash.new }
    end

    def [](k)
      v = super
      def v.or(other)
        empty? ? other : self
      end
      v
    end
  end
end

require 'test/unit'

class HashTest < Test::Unit::TestCase
  include Lang

  def test_hash_nil_value_or_returns_other
    h = Hash.new
    val = h['nonexistant']
    assert_equal 'val-was-false', (val.or 'val-was-false')
  end

  def test_hash_value_or_returns_value
    h = Hash.new
    h['a'] = 'a value'
    val = h['a']
    assert_equal 'a value', (val.or 'b value')
  end

  def test_hash_value_can_set_unset_stuff
    h = Hash.new
    h['a'] = 'value'
    assert_equal 'value', h['a'].or('nil get')
    assert_equal 'value', h['a']
  end

  def test_hash_value_can_set_unset_stuff_nested
    h = Hash.new
    h['a']['b'] = 'value'
    assert_equal 'value', h['a']['b']
    assert_equal 'value', h['a']['b'].or('nil get')
  end

  def test_hash_value_can_set_unset_stuff_more_nested
    h = Hash.new
    h['a']['b']['c'] = 'value'
    assert_equal 'value', h['a']['b']['c']
    assert_equal 'value', h['a']['b']['c'].or('nil get')
  end
end

Your refactoring





Format Copy from initial code

or Cancel