1

For example, I have a hash, in which updating is valid but adding new key is invalid.

opts = {
  url: 'www.google.com',
  local: 'disk',
  limit: 10
}

opts[:url] = 'www.facebook.com' # valid
opts[:other] = 'www.apple.com' # should raise an error
Run
  • 876
  • 6
  • 21
  • Nope, but you can build your own wrapper or a hash-like object. – Sergio Tulentsev Jul 28 '20 at 10:00
  • 4
    You could use a [`Struct`](https://ruby-doc.org/core-2.7.1/Struct.html). Its `[]=` method raises a `NameError` if the given member doesn't exist. – Stefan Jul 28 '20 at 10:05
  • @Stefan I used this comment in my answer since I really liked the solution. Let me know if you'd rather post your own, I can remove it. – Siim Liiser Jul 28 '20 at 10:37

2 Answers2

1

Using a built-in like Struct as @stefan suggested is a nice fast solution. Only the initiation is a bit weird.

opts = Struct.new(:url, :local, :limit).new(
  'www.google.com', 'disk', 10
)
opts[:url] = 'www.facebook.com'
opts[:other] = 'www.apple.com' # NameError: no member 'other' in struct
opts.to_h #=> {:url=>"www.facebook.com", :local=>"disk", :limit=>10}

However, if you want to you can also somewhat easily build your own solution to have more control over how it works and to be more similar to an actual hash.

class MyHash < Hash
  def initialize(hash)
    super()
    update(hash)
  end

  def []=(key, value)
    raise 'Unknown key passed to MyHash' unless key?(key) # or whatever error you want

    super
  end
end

opts = MyHash.new(
  url: 'www.google.com',
  local: 'disk',
  limit: 10
)
opts[:url] = 'www.facebook.com'
opts[:other] = 'www.apple.com' # RuntimeError: Unknown key passed to MyHash
opts #=> {:url=>"www.facebook.com", :local=>"disk", :limit=>10}

MyHash is a Hash for basically all purposes and can be used anywhere a regular hash can. Do note though, that this only overrides the direct setter ([]=). Indirectly assigning new values via update (merge!) for example still works.

Siim Liiser
  • 3,860
  • 11
  • 13
  • "for basically all purposes" - except those where you need to add a new key-value pair :) This, and the other point you mentioned make good argument _against_ subclassing Hash (violation of LSP) – Sergio Tulentsev Jul 28 '20 at 10:40
  • And the cumbersome instantiation of structs may be fixed in future ruby with [anonymous struct literals](https://supergood.software/ruby-anonymous-struct-literals/). – Sergio Tulentsev Jul 28 '20 at 10:42
  • 1
    Subclassing Ruby's core classes has many edge cases. This post from 2013 highlights some problems you might encounter: https://steveklabnik.com/writing/beware-subclassing-ruby-core-classes – Stefan Jul 28 '20 at 10:52
  • @SergioTulentsev: Regarding the LSP, note that the LSP talks about *subtyping*, not *subclassing*. While there are languages that confuse the two (e.g. C++, Java, C#), we should be clear about what we mean. For example, `StringIO` in Ruby is a *subtype* of `IO`, even though there is no inheritance relationship between them, other than that they both inherit from `Object`. – Jörg W Mittag Jul 28 '20 at 10:59
  • @JörgWMittag: good point, but in this example I think it's both? – Sergio Tulentsev Jul 28 '20 at 11:01
  • 1
    @SergioTulentsev: In Ruby, types only exist in the head of the programmer. So, this is a violation of LSP **IFF AND ONLY IFF** the programmer *thinks* that `MyHash` can be used everywhere a `Hash` can, without any changes. I'm not saying subclassing core classes is a good idea (it is not). I'm just saying that invoking the LSP which is all about types in the context of a language which doesn't even have types needs some additional thought. – Jörg W Mittag Jul 28 '20 at 11:07
1

While using Struct is a better option you can also consider to Freeze the Hash.

But you cannot update directly the key, so it doesn't work with all keys object, for example Integers, so you need to use keys as String, Arrays, etc:

h = {a: 'aa', b: 'bb', c: '1', ary: [1]}
h.freeze

h[:b].replace 'ss'
h[:c].replace '2'
h[:ary] << 10

h
#=> {:a=>"aa", :b=>"ss", :c=>"2", :ary=>[1, 10]}

h[:d] = '10'
#=> can't modify frozen Hash: {:a=>"aa", :b=>"ss", :c=>"2", :ary=>[1, 10]} (FrozenError)
iGian
  • 11,023
  • 3
  • 21
  • 36