Sunday, November 27, 2005

Enhance xmp (example printer)

อ่านเจอใน http://eigenclass.org/
Mauricio Fernandez. เขาเขียน utilities เล็กๆ
ที่ชื่อ Enhance xmp
เพื่อใช้ evaluate ตัวอย่าง code และแสดงค่า ณ ตำแหน่งต่างๆในโปรแกรม

ยกตัวอย่าง
สมมติเรามี snippet code ดังนี้
person1 = "Tim"
person2 = person1.dup
person1[0] = "J"
person1
person2


ถ้าเราต้องการแสดงให้เห็นว่า variable person1 และ person2
มีค่าเป็นอะไร เราก็มักจะเขียน comment ลงไปแบบนี้
person1 # => "Jim"
person2 # => "Tim"


ปกติเวลาเราเขียน comment แบบนี้
ก็มักจะใช้วิธี manual, ก็คือเปิด editor ออกมาใส่ค่าลงไปเอง

utilities ที่ Mauricio เขียน จะช่วย fill-in ค่าพวกนี้ให้เราโดยอัติโนมัติ
โดยเราเพียงแต่เขียนแบบนี้
person1 = "Tim"
person2 = person1.dup
person1[0] = "J"
person1 # =>
person2 # =>

เมื่อ run ผ่านโปรแกรม xmp
xmp ก็จะใส่ค่าลงไปให้เราเอง

ที่นี้ลองมาใล่ดูว่า source code เขาทำงานอย่างไร
technique ที่เขาใช้ก็คือ
เขาจะอ่าน source code ขึ้นมาก่อน
จากนั้นก็แทรก code ไปตัดต่อโปรแกรมบรรทัดที่ต้องการ
โดยสร้างตัวแปรขึ้นมารับผลลัพท์ที่ได้จาก evaluate บรรทัดนั้น
จากนั้นก็ให้พิมพ์ผลลัพท์ออกมาทาง stderr io
code = ARGF.read
idx = 0
newcode = code.gsub(/^(.*) # =>.*/) do |l|
expr = $1
(/^\s*#/ =~ l) ? l :
%!((#{VAR} = (#{expr}); $stderr.puts("#{MARKER}[#{idx+=1}] => " + #{VAR}.inspe
ct) || #{VAR}))!
end

จะเห็นว่าเขาอ่านโปรแกรมขึ้นมา แล้วใช้ gsub
ในการหาบรรทัดที่มี pattern ที่มี # =>
ถ้าเจอบรรทัดนั้น ก็จะทำการแทรก code เพิ่มเข้าไป
ลักษณะของ code ที่ได้ออกมาในตัวแปร newcode จะมีหน้าตาแบบนี้
person1 = "Tim"
person2 = person1.dup
person1[0] = "J"
((_xmp_1133097178_956604 = (person1); $stderr.puts("!XMP1133097178_657069![1] =>
" + _xmp_1133097178_956604.inspect) || _xmp_1133097178_956604))
((_xmp_1133097178_956604 = (person2); $stderr.puts("!XMP1133097178_657069![2] =>
" + _xmp_1133097178_956604.inspect) || _xmp_1133097178_956604))

จะเห็นได้ว่ามีการสร้างตัวแปรมารับ
แล้วก็พิมพ์ตัวแปรนั้นออกมาในรูปของ xxx[idx]=>value

พอแก้ code ไดดังนี้ เขาก็ทำการเรียกใช้งาน code ดังนี้
stdin, stdout, stderr = Open3::popen3("ruby", "-w")
stdin.puts newcode
stdin.close
output = stderr.readlines

เขาใช้ standard library open3 เข้ามาช่วย
(พวกที่ใช้ win32 จะไม่มี library ตัวนี้ให้)
โดยการเรียกโปรแกรม ruby ในอีก subprocess หนึ่ง
โดย subprocess จะรับข้อมูลผ่านทาง stdin
และให้ output กลับมาผ่านทาง stdout, stderr
ในกรณี xmp นี้ ก็จะใช้ข้อมูลจาก stderr เป็นหลัก

ตัวอย่าง stderr ที่ได้จาก program ข้างบน
!XMP1133102283_175558![1] => "Jim"
!XMP1133102283_175558![2] => "Tim"


ขั้นต่อไปก็เป็นการแปลความผลลัพท์ที่ได้จาก stderr
XMPRE = Regexp.new("^" + Regexp.escape(MARKER) + '\[([0-9]+)\] => (.*)')
results = Hash.new{|h,k| h[k] = []}
output.grep(XMPRE).each do |line|
result_id, result = XMPRE.match(line).captures
results[result_id.to_i] << result
end

จะเห็นว่ามีการ new Hash โดยใช้ block
(ที่เคยเขียนไปใน post เรื่อง Cache with Ruby Hash)
ถ้ามีการ access hash เมื่อไร ก็ให้สร้าง empty array เตรียมไว้ให้เลย
จะเห็นว่าใน code ก็จะ grep หาบรรทัดที่มีผลลัพท์
แล้วก็ทำการ put ผลลัพท์ลงไปใน hash (<< คือการ add ลง array)
ที่ต้องใช้ array มาเก็บ ก็เพราะบรรทัดที่อยู่ใน loop
มันจะถูก evaluate ได้หลายครั้ง
เช่น
i = 0
4.times {
i+=1 # => 1, 2, 3, 4
}

จะเห็นว่าบรรทัดใน loop มันจะมีค่าได้หลายค่า

ขั้นถัดไปก็ง่ายแล้ว ก็แค่ อ่าน code ขึ้นมาใหม่
จากนั้นก็ทำการ append ผลลัพท์ที่ได้ลงไป
ส่วนที่เหลือ ก็เป็นกรณี warning กับกรณี error แล้ว
ก็ใช้ technique แบบเดียวกัน เพียงแต่ง่ายกว่า
ตรง pattern ของ warning มันจะมีเลขบรรทัด
บอกมาเสร็จ ก็แค่แทรกลงไปให้ถูกบรรทัด
ส่วนกรณี error ถ้ามีก็ให้ต่อท้าย output ได้เลย

การได้ไล่โปรแกรมอย่างนี้ เป็นการเรียนรู้ที่ดีมาก
ผมว่าการจะเขียนโปรแกรมได้ดีนั้น ส่วนหนึ่งก็คือ
ต้องอ่าน source code คนอื่นเยอะๆ

Related link from Roti

No comments: