Metaprogramming, Domain Specific Language, และ Ruby

  • warning: realpath() [function.realpath]: SAFE MODE Restriction in effect. The script whose uid is 1005 is not allowed to access /tmp owned by uid 0 in /var/www/sites/sugree/codenone.com/subdomains/www/html/includes/file.inc on line 190.
  • warning: realpath() [function.realpath]: SAFE MODE Restriction in effect. The script whose uid is 1005 is not allowed to access /tmp owned by uid 0 in /var/www/sites/sugree/codenone.com/subdomains/www/html/includes/file.inc on line 190.

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

สิ่งที่เกิดขึ้นคือ

  1. ถ้าตอนนั้นยังไม่ีมีเมธอด translate ตัวแปรภาษาก็จะเรียกเมธอด method_missing ซึ่งจะนิยามเมธอด translate ขึ้นใหม่แล้วเรียกมัน
  2. เมื่อเมธอด translate ถูกเรียก มันจะสร้าง instance ของคลาส Translate ด้วย Translate.new
  3. แต่ Translate.new ถูกเราแฮ็กไปเมื่ีอกี้ จะสร้าง instance ของ Transform ขึ้นมาใหม่ เสร็จแล้วจะเรียก Mat4.translate 0.0, 1.0, 0.0 เพื่อสร้างเมทริกซ์แทนการย้ายไปทางแกน Y หนึ่งหน่วย เสร็จแล้วก็ยัดเมทริกซ์อันนั้นใส่ instance ของ Transform อันใหม่ที่เพิ่งถูกสร้างขึ้น แล้วจ่ายกลับไป

ด้วยเหตุนี้ผมจึงสามารถนิยามเมธอดอำนวยความสะดวกพวกนี้ได้อย่างง่ายดาย (ใช้เมธอดละสามบรรทัด... ความจริงก็ไม่ค่อยพอใจเท่าไหร่ เพราะคิดว่าน่าจะเขียน (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++ น่ะครับ อันนี้ช่วยไม่ได้จริืงๆ

sugree's picture

แล้ว C++ Binding มันทำให้ชีวิตเป็นยังไงบ้างครับ แบบว่าช่วงนี้กลับมาเขียน C++ เป็นกระสัย เห็น STL แล้วเครียด ยิ่งตอนเห็นปริมาณแรมที่โดนบริโภคแทบลมจับ

เท่าที่ผมรู้ C++ binding กับ ruby เขาจะใช้ SWIG กัน ตอนนี้ผมใช้ไม่เป็นครับ แต่มันไม่ีสำเร็จรูป (ไม่ต้องเขียน config file) เหมือนใช้ JRuby แต่คิดว่าไม่น่าจะใช้ลำบากมากนัก ถ้ามีเวลาก็อยากจะเรียนรู้ไว้ เพราะมันใช้ไ้ด้กับอีกตั้งหลายภาษาด้วย

เจ๋งๆ

เพิ่งเปิดอ่าน
เจ๋งมากครับ

ยอดเลยหนะคับ

เอ๋ ทำไมเพิ่งมาเห็น นี่มันเจ๋งมากๆเลยนะเนี่ย

ย้าย Codenone

ประกาศย้าย Codenone ไปใช้ Forum ของ Blognone แทนครับ ตามไปตั้งกระทู้ต่อได้ที่ Codenone Forum (รายละเอียดอ่านจากกระทู้ ย้าย Codenone ไปรวมกับ Blognone)

กระทู้เก่าๆ จะย้ายตามไปในภายหลัง ตอนนี้ปิดการโพสต์กระทู้ไว้ เหลือไว้เฉพาะอ้างอิงเท่านั้น