หากติดตาม Ruby 3x3 effort คงเคยได้ยิน Optcarrot คือ NES emulator เขียนด้วย Ruby มurniture
ผมอ่าน source Optcarrot เน้นรายละเอียดน่าสนใจ: ใช้ฟีเจอร์ของ Ruby hashes ที่มองข้ามบ่อย แต่มีประโยชน์มาก คือ ความสามารถใช้ object ใดๆ เป็น hash key
บริบท: NES Memory Mapping
ในฐานะ high-level programmers เราคิด memory คือ RAM แต่ระดับต่ำ "memory" มี用途อื่นมาก
การอ่าน/เขียน "memory" เป็นวิธี CPU ของ NES สื่อสารกับ GPU, control pads และอิเล็กทรอนิกส์พิเศษบน cartridge ขึ้นอยู่กับ address เรียก write_to_memory อาจ reset joystick, swap VRAM หรือเล่นเสียง
จะ implement ใน Ruby ยังไง?
Optcarrot ทำโดยเก็บ Method objects สองตัวสำหรับแต่ละ address 65536 addresses หนึ่ง getter หนึ่ง setter 長相似:
@getter_methods[0x0001] = @ram.method(:[])
@setter_methods[0x0001] = @ram.method(:[]=)
ปัญหา: Duplicate Objects
ปัญหาใช้ Object#method แบบนี้: สร้าง Method objects เยอะที่เหมือนกัน
เห็นจาก object_id:
> a = []
> a.method(:[]=).object_id
=> 70142391223600
> a.method(:[]=).object_id
=> 70142391912420
สอง Method objects มี object_id ต่างกัน จึงเป็น object ต่างกัน แม้ทำงานเหมือนกัน
ปกติไม่สนใจ Method objects เพิ่มเติม แต่ที่นี่มีหลายพันตัว
แก้ไข: Memoizing ผ่าน Hash
Optcarrot หลีกเลี่ยง duplicate Method objects ด้วยเทคนิคง่ายมากจนมองข้ามง่าย
ใช้ hash เพื่อ memoize และ deduplicate โค้ดลดรูปด้านล่างสาธิตเทคนิค:
def initialize
@setter_methods = []
@setter_cache = {}
...
end
def add_setter(address, setter)
# ไม่เก็บ duplicates
@setter_cache[setter] ||= setter
# ใช้ version ที่ deduped แล้ว
@setter_methods[address] = @setter_cache[setter]
end
ทำงานได้เพราะ Hash ไม่สนใจ object ใดๆ ที่ให้เป็น keys
หากสับสน ลองใน IRB ด้วย strings:
> cache = {}
> cache["foo"] ||= "bar"
=> "bar"
> cache["foo"] ||= "baz"
=> "bar"
ใน Ruby strings เป็น instances ของ class String กลไกที่ Ruby ใช้ string เป็น hash key 基本上เหมือนกับที่ใช้เก็บ Method object
Hash คำนวณ equality ยังไง
ใช้ non-string objects เป็น hash keys คำถาม: Hash รู้ยังไงว่าสอง objects เท่ากัน?
คำตอบ: ใช้เมธอด Object#hash เมธอดนี้เดินผ่าน object และ recursive generate hash คล้าย:
> a.method(:[]=).hash
=> 929915641391564853
เพราะ identical objects ผลิต hash values เหมือนกัน ใช้ทดสอบ equality ได้
a.hash == b.hash
น่าสนใจ: วิธีนี้เดียวกับที่เมธอด eql? ใช้:
a.eql?(b)
ใช้กับ Method objects ในตัวอย่างได้:
> a.method(:[]=).hash == a.method(:[]=).hash
=> true
สรุป
หลังคุ้นเคยกับ Ruby web development patterns มอง source optcarrot ดูว่า real-time non-web app ใช้ patterns ต่างกันอย่างไร ใน web app ผมสงสัยจะทำ array 65536 elements แต่ที่นี่ เป็นส่วนหนึ่งของ setup สำหรับ "desktop" app ทำให้มีเหตุผล
หากมีคำถามหรือความคิดเห็น ติดต่อได้ที่ starr@honeybadger.io หรือ @StarrHorne บน Twitter