[Ruby] Tạo một Hash với chiều sâu vô hạn
Bài đăng này đã không được cập nhật trong 9 năm
Đọc và dịch theo ý hiểu của bài viết sau http://firedev.com/posts/2015/bottomless-ruby-hash/
Vào những ngày khác nhau, có nhiều người hỏi rằng nếu có một cách mù quáng nào đó để gán giá trị lồng nhau cho Ruby Hash mà không càn tạo từng key. Hóa ra là có, và nó có những thú vị, tuy nhiên những thú vị đó có thể gây ra các tác dụng phụ. Chào mừng đến với Bottomless Hash.
Đầu tiên, chúng ta hãy thử theo cách thông thường khi mà gán giá trị vào một Hash.
params = {}
params[:world][:vietnam] = :hanoi
=> NoMethodError: undefined method `[]=' for nil:NilClass
Như các bạn đã thấy, chúng sẽ không thể hoạt động và raise ra lỗi như trên. May mắn thay, Hashes trong Ruby có thể khởi tạo với giá trị mặc định ban đầu. Điều đầu tiên là hãy thử nó, trông nó khá là rõ ràng. Hãy khởi tạo một Hash mới và để giá trị mặc định ban đầu là một hash rỗng.
params = Hash.new({})
params[:world][:vietnam] = :hanoi
params[:world]
=> {:vietnam=> :hanoi}
Trông nó có vẻ hợp lý. Nhưng chúng ta hãy đào sâu thêm một chút. Ta khởi tạo một giá trị mới cho hash vừa rồi như sau
params[:world][:thailand] = :bangkok
Bây giờ hãy thử xem params
có những giá trị nào:
params
=> {}
Tại sao nó lại trả về kết quả rỗng như vậy??? Ta hãy add thêm một số key khác vào trong nó.
params[:underworld] = :hell
Và kiểm ra lại params
params
=> {:underworld=>:hell}
Chuyện gì đang xảy ra vậy? Đã có một phép thuật xấu xa nào ở đây à? Không hẳn là như vậy. Đầu tiên :world
key được khởi tạo với giá trị mặc định là một Hash rỗng. Nó rất dễ dàng để truy cập, Vì một hash vẫn được trả về khi mà nó không có key nào. Tuy nhiên tất cả thành phố của chúng ta đều có giá trị trong cả 2 thế giới (:world, và :underworld
) chúng ta đã khởi tạo
params[:world][:thailand]
=> :bangkok
params[:underworlds][:vietnam]
=> :hanoi
Vâng, chúng ta đã biết và cần phải fix nó. Khởi tạo một Hash mới cho các giá trị, chúng ta cần vượt qua được một block, block đó chấp nhận 2 biến - một là Hash cho chính nó, và key để truy cập tới nó. Bây giờ hãy khởi tạo Hash rỗng với key và giá trị cho nó.
params = Hash.new do |hash, key|
hash[key] = Hash.new
end
params[:world][:thailand]=:phuket
Bây giờ hãy kiểm tra lại giá trị của hash mới khởi tạo
params
=> {:world=>{:thailand=>:phuket}}
Thật tuyệt vời đúng không nào. Okay, nhưng mà chuyện gì sẽ xảy ra nếu chúng ta add thêm một level mới?
params[:asia][:thailand][:bangkok] = :chao_praya
=> NoMethodError: undefined method `[]=' for nil:NilClass
Oh không, không phải một lần nữa chứ. Chúng ta có thể làm gì giờ? Hãy add thêm một tầng mới cho hash khởi tạo. Vì vậy các Hash nồng nhau có thể lần lượt tạo thêm hashes:
params = Hash.new do |hash0, key0|
hash0[key0] = Hash.new do |hash1, key1|
hash1[key1] = Hash.new
end
end
params[:asia][:thailand][:moscow] = :moscow_river
Nó hoạt động, nhưng điều gì xảy ra nếu ta add thêm chiều sâu cho hash đó?
params[:asia][:thailand][:bangkok][:river] = :chao_praya
=> NoMethodError: undefined method `[]=' for nil:NilClass
Okay, bây giờ chúng ta cần giải quyết điều này một lần và cho tất cả. Bây giờ chúng ta hãy gộp các chức năng cần thực hiện vào một function và sau đó đưa chúng tới nơi mà chúng ta cần sử dụng. Điều chúng ta cần là một procedure(thủ tục), nó sẽ trả về một hash mới với procedure giống trước đó, được ẩn bên trong chờ đợi cho một key mới sẽ được khởi tạo.
Cái trình tự đó trông sẽ như thế nào? Nó khá là quen thuộc với thực tế. Chúng ta chỉ cần đóng gói nó vào một lambda
và dùng biểu tượng &
để đẩy nó vào trong Hash khi khởi tạo.
procedure = lambda do |hash, key|
hash[key] = Hash.new(procedure)
end
params = Hash.new(&procedure)
params[:russia][:moscow] = :moscow_river
params
=> {:russia=>{:moscow=>:moscow_river}}
Okay vấn đề đó đã được giải quyết, bây giờ ta sẽ làm cho nó chặt chẽ hơn => chúng ta sẽ không cần phải tạo một lambda trước khi khởi tạo một Hash. Ruby Hash có một phương thức là default_proc
, phương thức này cho phép chúng ta truy cập vào các block hash khi khởi tạo Hash.
params = Hash.new {|h, k| h[k] = Hash.new(&h.default_proc)}
params[:world][:thailand][:bangkok][:bangna]
params
=> {:world=>{:thailand=>{:bangkok=>{:bangna=>{}}}}}
Nó thật tuyệt vời đúng không, nhưng quan điểm thực tế của một hash không đáy là gì? Điểm thú vị mà nó gây ra ở đây là: nó sẽ không bao giờ bị lỗi khi bạn đọc một giá trị nào đó.
params[:i][:dont][:know]
=> {}
Và vẻ đẹp của nó là, bạn có thể sát nhập bất kì hash nào đó với nó để sản xuất ra một phiên bản hash không đáy. Vì vậy bạn có thể truy cập một cách mù quáng đến các keys của hash.
unknown = { key: :value }
bottomless = params.merge unknown
bottomless[:missing][:value]
=> {}
Không có câu trả lời cho câu hỏi, độ dài của chuỗi sẽ là bao nhiêu, Hash không đáy sẽ không raise ra một lỗi nào. Nó trả về một hash rỗng thay vì giá trị nil
, cái nào là sự thật1? Nhưng nó có thể được check với function empty?
ngay cả trong ruby đơn thuần.
Như đã được đề cập trước đó, chúng ta có thể đóng gói các hành vi vào một Class
và nó sẽ trả về cho chúng ta một Bottomless rỗng (hash không đáy rỗng) hoặc nó sẽ chuyển đổi một hash sang một phiên bản bottomless mới.
class BottomlessHash < Hash
def initialize
super &-> h, k {h[k] = self.class.new}
end
def self.from_hash(hash)
new.merge(hash)
end
end
Bạn có thể tham khảo một số test function dưới đây:
class BottomlessHash < Hash
def initialize
super &-> h, k { h[k] = self.class.new }
end
def self.from_hash(hash)
new.merge(hash)
end
end
class Hash
def bottomless
BottomlessHash.from_hash(self)
end
end
Test
describe BottomlessHash do
subject { described_class.new }
it 'does not raise on missing key' do
expect do
subject[:missing][:key]
end.to_not raise_error
end
it 'returns an empty value on missing key' do
expect(subject[:missing][:key]).to be_empty
end
it 'stores and returns keys' do
subject[:existing][:key] = :value
expect(subject[:existing][:key]).to eq :value
end
describe '#from_hash' do
let (:hash) do
{ existing: { key: { value: :hello } } }
end
subject do
described_class.from_hash(hash)
end
it 'returns old hash values' do
expect(subject[:existing][:key][:value]).to eq :hello
end
it 'provides a bottomless version' do
expect(subject[:missing][:key]).to be_empty
end
it 'stores and returns new values' do
subject[:existing][:key] = :value
expect(subject[:existing][:key]).to eq :value
end
it 'converts nested hashes as well' do
expect do
subject[:existing][:key][:missing]
end.to_not raise_error
end
end
end
=> Bottomless là tiện cho việc xử lý cũng như là khi giao dịch với các cấu trúc lồng nhau từ thế giới bên ngoài, nhưng mà cũng có một điểm đó là, khi mà bạn gọi một key không có trong hash => nó sẽ mặc định thêm key đó vào hash của bạn (điều này thực sự cũng không có ảnh hưởng gì nhiều đến code của bạn.)
All rights reserved