[Ruby] Tạo một Hash với chiều sâu vô hạn
Đọ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 ...
Đọ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.)