Metaprogramming และ domain specific language เป็น buzzwords ที่กำลังฮ็อตฮิตกันในหมู่ผู้ใช้ ruby เท่าที่ผมอ่านบล็อกของโปรแกรมเมอร์คนอื่นๆ มา เขาต่างสรรเสริญว่า ruby ช่วยทำให้การสร้าง domain specific language ง่ายขึ้นจม เพราะไวยากรณ์ที่ยืดหยุ่น และความสามารถในการทำ metaprogramming ของตัวภาษา เมื่ออาทิตย์ก่อนผมได้ลองใช้ ruby สร้าง domain specific language ด้วยตนเอง บอกได้เต็มปากเต็มคำครับว่ามันดีงามอย่างที่เขาว่าจริงๆ
Domain specific language ที่ผมสร้างเป็นภาษาที่ใช้บรรยายฉาก 3D สำหรับ ray caster ตัวเล็กๆ ที่ตอนแรกผมกะจะเขียนใช้สอนในคอร์สคอมพิวเตอร์กราฟิกของมหาวิทยาลัยแห่งหนึ่ง (ตอนนี้โครงการนี้ต้องเป็นหมันไป เพราะอาจารย์รุ่นพี่บอกให้เปลี่ยน code ที่เขียนด้วย Java ให้เป็น C++ แทน) หน้าตาของภาษาก็จะประมาณนี้ครับ
scene do background rgb(0.0, 0.0, 0.0) camera perspective square do color rgb(0.0, 1.0, 1.0) end triangle do a vec(0.0, 2.0, 0.0) b vec(1.0, 1.0, 0.0) c vec(-1.0, 1.0, 0.0) color rgb(1.0, 0.0, 1.0) end end
ซึ่งมันก็เป็นโปรแกรม ruby ดีๆ นั่นเอง คนที่เคยใช้ POV-Ray อาจจะรู้สึกว่ามันคล้ายๆ กับ scene description language ของเขา (ผมไปลอกเขามา)
ที่ผมไม่อยากเขียน parser มา parse ไฟล์ scene description เองก็เพราะยังใช้ parser generator ยังไปเป็น อีกอย่างหนึ่ง ถ้าเขียนด้วย ruby แล้วเราสามารถใส่ลูกเล่นซับซ้อนอื่นๆ ลงไปใน scene description ได้โดยไม่ต้องเสียแรง เช่น
scene do background rgb(0.0, 0.0, 0.0) camera perspective 8.times do |i| theta = Math::PI/4.0*i x = 4 * Math.cos theta y = 4 * Math.sin theta translate x, y, 0.0 do square do color rgb(1.0, 1.0, 1.0) end end end end
ฉากข้างบนนี้จะมีทรงกลมสีขาว 8 ลูกวางอยู่บนจุดยอดของแปดเหลี่ยมด้านเท่ารัศมี 4 หน่วยที่มีจุดศูนย์กลางอยู่ที่ (0,0,0) นอกจากจะใช้ loop ใน ruby แล้วเรายังสามารถใ้ช้โครงสร้างและฟังก์ชันอื่นๆ ที่ ruby มีไว้ให้ใช้ได้ทั้งหมด!
code ของระบบ scene description language นี้สั้นมากเมื่อเทียบกับความยาวถ้าเราจะเขียนเองใน java หรือ c++ แต่ถ้าเอามาแปะไว้ที่นี่จะยาวเกินไป ถ้าใครสนใจจะอ่านแบบเต็มๆ ก็ไปดูได้ที่ http://svn.moekaku.com/418681dev/rt-01/trunk/src/sdl/sdl.rb ครับ
ที่น่าสนใจใน code คือเมธอด scene ซึ่ง นิยามไว้ว่า
def scene(&blk) Scene.create &blk end
กล่าวคือมันจะรับ block มาหนึ่ง block แล้วผ่าน block ไปให้กับ Scene.create ซึ่งมีนิยามดังต่อไปนี้
class Scene < SingleContainer def Scene.create(&blk) @the_scene = Scene.new @the_scene.instance_eval &blk end end
Scene.create ไม่ได้ทำอะไรไปมากกว่าสร้าง instance ของคลาส Scene ขึ้นมาใหม่ แล้วรัน block ที่ได้รับมาโดยใช้ binding ของ instance ที่ถูกสร้างมานั้นผ่านเมธอด instance_eval กล่าวคือ ฟังก็ชัน background, rgb, sphere, triangle ฯลฯ ที่ปรากฎระหว่าง scene do กับ end นั้นเป็นเมธอดของคลาส Scene (เกือบ)ทั้งหมด
เมธอดพวกนี้บางตัวก็ถูกนิยามแบบธรรมดาๆ เช่น เมธอด perspective
class Scene < SingleContainer def perspective(&blk) PerspectiveCamera.new &blk end end
บางเมธอด เช่น camera, background ซึ่งมีหน้าที่เหมือน "ฟีลด์" ของ object ในคลาส Scene ก็ถูำกนิยามโดยใช้คลาสเมธอด fields ดังนี้
class Scene < SingleContainer fields :background, :camera end
ซึ่ง fields มีนิยามดังต่อไปนี้ (ต้องย้อนกลับไปใน class hierachy กันไกลหน่อยนะครับ: Scene < SingleContainer < Container < Object3D < SDLNode)
class SDLNode def self.fields(*args) args.each do |name| define_method name do |val| instance_variable_set( "@#{name}", val ) end end end end
ความจริงแล้ว fields นั้นไม่ต่างอะไรกับ attr_writer มาก ต่างกันตรงที่ attr_writer นิยามเมธอด "name=" ดังนั้นถ้าเราใช้ attr_writer เราจะต้องพิมพ์ background = rgb(0.0, 0.0, 0.0) แทนที่จะเป็น background rgb(0.0, 0.0, 0.0) อย่างที่เห็นข้างต้น เนื่องจากผมไม่ชอบให้มีเครื่องหมายเท่ากับในคำบรรยายฉาก เลยเขียน fields ใช้แทน attr_writer เสีย
เมธอดที่เหลือ เช่น sphere, triangle, translate ฯลฯ เป็นเมธอดที่สร้างวัตถุสามมิติประเภทต่างๆ แล้วเอาไปใส่ไว้ใน Scene โดยวัตถุสามมิติเหล่านี้แต่ละชนิดมีคลาสเป็นของตนเอง เมธอดเพื่อสร้างวัตถุเหล่านี้ถูกสร้างขึ้นแบบ on demand โดยใช้กลไกของเมธอด method_missing ดังต่อไปนี้:
class Container < Object3D def method_missing(func_name, *args, &blk) if @@creator_func.has_key?(func_name) the_class = @@creator_func[func_name] self.class.class_eval "def #{func_name}(*args, &blk)\n" + "new_instance = #{the_class}.new *args\n" + "new_instance.instance_eval &blk if block_given?\n" + "add new_instance\n" + "end" send(func_name, *args, &blk) end end end
@@creator_func เป็น Hash ที่แมพชื่อเมธอดที่ควรจะมีไปยังคลาสที่เมธอดนั้นทำหน้าที่สร้าง instance ยกตัวอย่างเช่น @@creator_func[:triangle] = Triangle เป็นต้น ใน code ข้างบน เรานำชื่อเมธอดที่รับมาจาก method_missing ไปค้นหาคลาสที่เหมาะสม (ใส่ไว้ในตัวแปร the_class) เมื่อได้มาแล้วเราก็จะนิยามเมธอดขึ้นมาใหม่โดยใช้ชื่อเมธอดที่ได้รับ เมธอดใหม่ที่ว่าจะรับ argument หลายๆ ตัวและ block มา block หนึ่ง แล้วมันจะสร้าง instance ตัวใหม่ของคลาส the_class แล้วทำการรัน block ที่ได้รับมาภายใต้ binding ของตัวมันเอง เสร็จแล้วเจ้า instance ใหม่ก็จะถูกใส่เพิ่มเข้าไปใน Scene ด้วยเมธอด add
(มาเติมแล้วหลังจากวันแรก...)
ส่วนการใส่ข้อมูลที่ว่าชื่อเมธอดใดใช้สร้าง instance ของคลาสใดลงใน @@creator_func เราทำโดยผ่านคลาสเมธอด creator_func ซึ่งมีนิยามดังต่อไปนี้ครับ
class Object3D < SDLNode @@creator_func = {} def self.creator_func(*func_name) if (func_name.length == 0) return @@creator_func else @@creator_func[func_name[0]] = self end end end
โดยไม่ ดังนั้นถ้า subclass ของ Object3D ใด ประสงค์จะให้มีเมธอดสำหรับสร้าง instance ของตัวเอง ก็สามารถเรียก creator_func <ชื่อฟังก์ชัน> ได้ เช่น
class Sphere < Shape creator_func :sphere end
ส่วนคลาสที่เราไม่ต้องการให้ผู้ใช้สร้างแล้วเอาไปใส่ในวัตถุอื่นๆ เราก็ไม่ต้องเรียก creator_func เสีย เช่น คลาส Scene หรือคลาสที่เราจะไม่สร้าง instance อย่าง Object3D, Container, ฯลฯ
แฮ็กอันสุดท้ายที่อยากจะพูดถึง คือ คลาสในกลุ่ม Transform ครับ คลาส Transform นี่ใช้กำหนดการแปลงในสามมิติ ซึ่งสามารถแทนได้ด้วยใช้เมทริกซ์ 4x4 อันหนึ่ง ดังนั้น instance ทุกตัวของคลาสนี้จะเก็บเมทริกซ์นี้ไว้
class Transform < SingleContainer fields :matrix end
แต่ผมไม่อยากให้คนใช้ต้องมาป้อนเมทริกซ์เองทุกๆ ครั้งที่จะสร้าง Transform จึงอยากให้มีเมธอดอำนวยความสะดวก เช่น translate, rotate, scale, ฯลฯ ยกตัวอย่างเช่น ถ้าผู้ใช้อยากจะสร้างการหมุน 30 องศาโดยใช้เวกเตอร์ (1.0, 1.0, 1.0) เป็นแกน เขาควรจะสามารถสั่ง:
rotate vec(1.0, 1.0, 1.0), 30.0 do ... end
ซึ่งจะได้ผลลัพธ์ออกมาเป็น instance ของคลาส Transform ที่มีเมทริกซ์ที่ใช้แทนการหมุน 30 องศา รอบ (1.0, 1.0, 1.0)
เมธอดอำนวยความสะดวกของผมมีอยู่ด้วยกันถึง 12 เมธอด ซึ่งมีชื่อเหมือนกับเมธอดสร้างเมทริกซ์ 12 เมธอดในคลาส Mat4 เป๊ะๆ ถ้าจะเขียนเมธอดพวกนี้ใส่คลาส Container (ซึ่ง subclass ของมันทั้งหมดสามารถเรียกเมธอดเหล่านี้ได้) เองทั้งหมดคงจะเมื่อยมือเอาการ อย่างไรก็ดี ruby ช่วยให้ผมลดงานขั้นตอนนี้ไปได้เยอะมากครับ
โดยขั้นแรกผมสร้างคลาส SpecificTransform แล้ว override เมธอด creator_func ดังต่อไปนี้:
class SpecificTransform < SingleContainer def self.creator_func(*func_name) if (func_name.length == 0) return @@creator_func else @@creator_func[func_name[0]] = self singleton_define :new do |*args| result = Transform.new result.matrix( Mat4.send(func_name[0], *args) ) return result end end end end
creator_func ของ SpecificTransform จะทำงานเหมือนกับ creator_func เดิม แต่นอกจากจะ update ค่าของ @@creator_func แล้ว มันยังนิยามเมธอด new ของคลาสตัวเองใหม่ ให้แทนที่จะคืน instance ของคลาสตัวเองออกมา กลับไปคืน instance ของ Transform ซึ่งมีเมทริกซ์ซึ่งสร้างจากเมธอดที่มีชื่อเดียวกับเมธอดที่(ควรจะ)ใช้สร้าง instance ของ subclass ของ SpecificTransform
ยกตัวอย่างเช่น ผมสามารถนิืยาม
class Translate < SpecificTransform creator_func :translate end
แล้ว ถ้าใน scene description มีการเรียก:
translate 0.0, 1.0, 0.0 do ... end
สิ่งที่เกิดขึ้นคือ
ด้วยเหตุนี้ผมจึงสามารถนิยามเมธอดอำนวยความสะดวกพวกนี้ได้อย่างง่ายดาย (ใช้เมธอดละสามบรรทัด... ความจริงก็ไม่ค่อยพอใจเท่าไหร่ เพราะคิดว่าน่าจะเขียน (meta)code ไว้สร้าง class พวกนี้ได้เลย ใครสนใจเอาไปลองก็ได้นะครับ)
เอวังก็มีด้วยประการฉะนี้แล สวัสดีครับ
อ้อ... ใครอยากเอา ray caster ไปเล่นก็ svn checkout http://svn.moekaku.com/418681dev/rt-01/trunk/ ไปได้ แต่ไอ้ rt-01 นี้มันยังทำอะไรไม่ได้มากนะครับ ไลบรารีที่ต้องใ้ช้ได้แก่ JRuby (http://jruby.codehaus.org) และ javax.vecmath (มีมาให้พร้อมกับตอน checkout ครับ แต่ต้องปรับค่า CLASSPATH กันเอาเองถ้าจะ compile นะครับ)
กระทู้เก่าๆ จะย้ายตามไปในภายหลัง ตอนนี้ปิดการโพสต์กระทู้ไว้ เหลือไว้เฉพาะอ้างอิงเท่านั้น
Post นี้ยกให้เป็น post เยี่ยมยอดประจำปีนี้เลย
อ่านตามแล้วสนุกมาก
อ่าน code แล้วชอบหลายตัวเลย
เช่น การ register ตัวเองของพวก Spehere, Square, Triangle เข้ากับ @@creator_func
>> ตอนนี้โครงการนี้ต้องเป็นหมันไป เพราะอาจารย์รุ่นพี่บอกให้เปลี่ยน code ที่เขียนด้วย Java ให้เป็น C++ แทน)
เขาให้เหตุผลว่าอย่างไรหรือครับ
อ้อ นักเรียนกลุ่มที่มาเรียนอยากใช้ C++ น่ะครับ อันนี้ช่วยไม่ได้จริืงๆ
แล้ว C++ Binding มันทำให้ชีวิตเป็นยังไงบ้างครับ แบบว่าช่วงนี้กลับมาเขียน C++ เป็นกระสัย เห็น STL แล้วเครียด ยิ่งตอนเห็นปริมาณแรมที่โดนบริโภคแทบลมจับ
เท่าที่ผมรู้ C++ binding กับ ruby เขาจะใช้ SWIG กัน ตอนนี้ผมใช้ไม่เป็นครับ แต่มันไม่ีสำเร็จรูป (ไม่ต้องเขียน config file) เหมือนใช้ JRuby แต่คิดว่าไม่น่าจะใช้ลำบากมากนัก ถ้ามีเวลาก็อยากจะเรียนรู้ไว้ เพราะมันใช้ไ้ด้กับอีกตั้งหลายภาษาด้วย
เจ๋งๆ
เพิ่งเปิดอ่าน
เจ๋งมากครับ
ยอดเลยหนะคับ
เอ๋ ทำไมเพิ่งมาเห็น นี่มันเจ๋งมากๆเลยนะเนี่ย