Theppitak's blog

My personal blog.

10 ตุลาคม 2559

Crypto Disk Shutdown Problem Workaround (2)

จาก blog ที่แล้ว ที่ได้เขียนถึงการแก้ขัดปัญหา shutdown เครื่อง Debian ที่ใช้ sysvinit ไม่ลง อันเนื่องมาจากการค้างที่ขั้นตอนการปิด crypto disk ด้วยการไปแก้ไฟล์ /lib/cryptsetup/cryptdisks.functions นั้น หลังจากนั้นก็ได้ครุ่นคิดหาวิธีที่เหมาะสมกว่านั้น จนถึงจุดที่คิดว่าน่าจะลองเสนอใน Debian ได้

ปัญหาของการแก้แบบเดิมก็คือ:

  1. ไม่ idempotent เพราะใน do_stop() ไป stop ดีมอน แต่ใน do_start() ไม่ได้สั่ง start ดีมอนใหม่ เพราะสมมุติว่ามันถูก start มาแล้ว และทำแค่หา PID ของดีมอนมาใส่ใน omit file เท่านั้น ซึ่งจะทำให้เกิดปัญหาดีมอนตายได้หากผู้ใช้สั่ง stop, start หรือ restart cryptdisks ขณะที่เครื่องทำงานอยู่ ไม่ใช่ผ่านการ shutdown หรือ reboot ตามปกติ
  2. อาจกระทบผู้ใช้ init ระบบอื่น เพราะฟังก์ชันนี้อาจถูกเรียกใช้จากระบบ init อื่นก็ได้ ทั้งที่ระบบเหล่านั้นอาจไม่ได้มีปัญหานี้ และอาจเกิดผลกระทบไม่พึงประสงค์ได้
  3. ไม่สวยและไม่ปลอดภัย การอ่านค่า PID ด้วย ps, grep, awk ดูเยิ่นเย้อและไม่ปลอดภัย เพราะ awk อยู่ใน /usr/bin ซึ่งเสี่ยงต่อการเรียกในระหว่างการบูตที่อาจจะยังมีแค่ /bin หรือ /sbin ให้ใช้

ปัญหาการกระทบระบบ init อื่น ทำให้มองไปที่การแก้ /etc/init.d/* แทน ซึ่งในอีกแง่หนึ่ง ก็เป็นการแก้ในระดับบนซึ่งเป็นระดับเดียวกับที่ service อื่น ๆ ใช้จัดการกับ sendsigs อยู่แล้ว จึงสรุปว่าน่าจะเหมาะสมด้วยประการทั้งปวง

เบื้องต้นผมจึงไปแก้ที่ /etc/init.d/cryptdisks โดยทำหลังจาก do_start เสร็จแล้ว:

--- /etc/init.d/cryptdisks.orig 2016-10-08 17:14:02.087652932 +0700
+++ /etc/init.d/cryptdisks 2016-10-10 09:42:15.301303974 +0700
@@ -31,9 +31,19 @@
  ;;
 esac
 
+UDEVD_DAEMON="/lib/systemd/systemd-udevd"
+
 case "$1" in
 start)
  do_start
+
+ # Omit udev daemon on halt to allow cryptsetup to do the close
+ UDEVD_PID=$(pidof $UDEVD_DAEMON)
+ if [ ! -z "$UDEVD_PID" ]; then
+  OMITDIR=/run/sendsigs.omit.d
+  mkdir -p $OMITDIR
+  echo $UDEVD_PID > $OMITDIR/systemd-udevd
+ fi
  ;;
 stop)
  do_stop

สังเกตว่าในรอบนี้ผมไม่ได้สั่ง stop ดีมอนหลัง do_stop อีกแล้ว เพราะทำให้เกิดปัญหาไม่ idempotent ดังที่กล่าวไปแล้ว อีกทั้งมันไม่จำเป็นเลย เพราะ service script ของ udev เองก็จะทำหน้าที่ stop ดีมอนนี้ให้อยู่แล้ว สิ่งที่จำเป็นมีแค่ชะลอไม่ให้ดีมอนถูกฆ่าจนถึงตอนนั้นก็พอ

และสังเกตว่าผมไม่ได้ใช้สคริปต์เยิ่นเย้อในการหา PID ของดีมอนอีกแล้ว ในเมื่อสามารถใช้ /bin/pidof ที่สั้นกระชับและปลอดภัยกว่า

อย่างไรก็ดี สิ่งที่ยังคาใจอยู่ในขั้นนี้ก็คือ มันควรแก้ที่ udev จะเหมาะสมที่สุดถ้าทำได้ เพราะเป็นเจ้าของดีมอนเอง การแยกตรรกะการละเว้นดีมอนมาไว้ที่ service อื่น ดูไม่สวยเท่าไร

ในรอบที่แล้วผมทำกับ udev ไม่สำเร็จ คาดว่าเป็นเพราะไปแทรกโค้ดที่ลำดับต้น ๆ ตั้งแต่เพิ่งเรียกดีมอนใหม่ ๆ ซึ่ง file system อาจจะยังไม่พร้อม อีกทั้งการเรียกใช้ awk จาก /usr/bin ก็ยังสุ่มเสี่ยงมากในระหว่างบูตอีกด้วย

รอบนี้ผมจึงลองใหม่ให้หายคาใจ โดยไปแทรกโค้ดในลำดับท้ายสุด หลังจาก Waiting for /dev to be fully populated... เสร็จแล้ว:

--- /etc/init.d/udev.orig 2016-10-10 09:40:47.937302585 +0700
+++ /etc/init.d/udev 2016-10-10 09:42:59.701304680 +0700
@@ -203,6 +203,14 @@
     else
  log_action_end_msg 0 'timeout'
     fi
+
+    # Omit systemd-udevd on halt to allow cryptsetup to do the close
+    UDEVD_PID=$(pidof $DAEMON)
+    if [ ! -z "$UDEVD_PID" ]; then
+        OMITDIR=/run/sendsigs.omit.d
+        mkdir -p $OMITDIR
+        echo $UDEVD_PID > $OMITDIR/udev
+    fi
     ;;
 
     stop)

ตรวจความเรียบร้อยแล้วก็ลองรีบูตเครื่องดู... ผ่าน!

ถึงจุดนี้ ก็เลยคิดว่าควรลองเสนอเข้าบั๊ก Debian #791944 ดู (ข้อความ #103) เพื่อให้ผู้ดูแลพิจารณาตามความเหมาะสม

ป้ายกำกับ: ,

07 ตุลาคม 2559

Crypto Disk Shutdown Problem Workaround

เมื่อเช้านี้รีบูตเครื่องหลายรอบมาก กว่าจะได้เริ่มงาน เหตุเพราะถูกบั๊ก Debian #839888 ของ cryptsetup กัด สุดท้ายเลยได้นั่งลงแก้ปัญหา crypto disk ที่ทำให้ shutdown เครื่องไม่ลงมาเป็นปีเสียที

ผมใช้ encrypted file system ในโน้ตบุ๊กเครื่องปัจจุบันมาตั้งแต่เริ่มแรก แต่มาเริ่มมีปัญหาในรอบ Stretch นี้น่าจะเกือบปีแล้ว คือในการ shutdown เครื่องมันจะมาค้างตรงขั้น Stopping remaining crypto disks... แล้วก็ไม่ไปไหน แม้จะลองทิ้งไว้เป็นชั่วโมงก็ตาม สุดท้ายต้องยอมกดปุ่ม power ค้างเพื่อตัดไฟให้เครื่องมันดับ แล้วก็ให้มันมา recover journal ตอนเปิดเครื่องใหม่เอา

ปล่อยให้เป็นอย่างนี้มาเป็นปี เพราะยังไม่มีเวลาไปนั่งแก้ปัญหา ตัว cryptsetup เองก็มีอัปเดตใน Debian มาหลายรุ่น แต่ละรุ่นก็ได้แต่ภาวนาว่าจะมีการแก้ปัญหานี้ แต่ก็ไม่มี และในเมื่อเครื่องมันยังเปิดใช้งานได้ทุกวัน ผมเลยปล่อยให้มันเป็นอย่างนี้มาเรื่อย ๆ (ซึ่งไม่ควร)

จนกระทั่งเมื่อเช้ามันบูตไม่ขึ้น! เพราะรุ่น 2:1.7.2-1 มีบั๊กในสคริปต์ที่พิมพ์ผิด ซึ่งเป็นรายงานที่ผมเพิ่งจะได้อ่านหลังจากที่แก้ปัญหาด้วยตัวเองจนบูตเครื่องขึ้นมาต่อเน็ตได้แล้ว และปรากฏว่าแพตช์ที่แก้เองไปก็เหมือนกันกับแพตช์ในรายงานบั๊กเป๊ะ

แต่ก็ไม่ได้ทำให้ปัญหาเครื่องค้างตอน shutdown หายไป แต่ไหน ๆ ก็มือเปรอะไปแล้ว เลยนั่งไล่ต่อ จนกระทั่งไปพบคำสนทนาที่สาวไปจนถึงบั๊ก Debian #791944 ซึ่งทำให้รู้ว่าปัญหานี้เกิดเฉพาะกับระบบที่ใช้ sysvinit เท่านั้น ไม่เกิดกับ systemd และในความเห็น #72 Guilhem Moulin ได้พบว่าในขั้นตอนการ shutdown นั้น ดีมอน systemd-udevd ได้ถูก kill ไปก่อนที่จะถึงขั้นปิด crypto disk ทำให้คำสั่ง cryptsetup luksClose "$dst" ค้าง!

พิษ systemd อีกแล้วครับพี่น้อง!

แต่อย่างไรก็ดี ในเมื่อได้เบาะแสอย่างนี้แล้ว ถ้าเราสามารถทำให้ดีมอน systemd-udevd อยู่รอดจากการฆ่าจนถึงขั้นปิด crypto disk ได้ ปัญหาของเราก็จะหมดไป

แน่นอนว่าวิธีที่ถูกหลักการนั้น ต้องไปแก้ที่ระบบ init และสถานะล่าสุดของบั๊กดังกล่าว ก็ได้ reassign ไปให้แพกเกจ initscripts แล้ว แต่เครื่องผมล่ะ? ขอแก้ปัญหาเฉพาะหน้าระหว่างรอละกัน

วิธีแก้ขัดของผมคือ ในขั้นตอน sendsigs ของการ shutdown เพื่อ Asking all remaining processes to terminate นั้น เราสามารถละเว้นบางโพรเซสจากการรับ SIGTERM ได้ โดยเพิ่มไฟล์ที่เก็บ PID ไว้ในไดเรกทอรี /run/sendsigs.omit.d/ ตัวอย่างของ service ที่ทำแบบนี้ก็เช่น rsyslog และ wpasupplicant ผมก็จัดการยืมมาใช้กับ cryptdisks เสีย โดยแก้ไฟล์ /lib/cryptsetup/cryptdisks.functions ในฟังก์ชัน do_start() ให้เพิ่ม PID ของ systemd-udevd ไว้ในรายชื่อโพรเซสละเว้น:

--- cryptdisks.functions.orig   2016-10-07 12:11:36.104693329 +0700
+++ cryptdisks.functions        2016-10-07 13:53:10.640533260 +0700
@@ -758,6 +758,11 @@ do_start () {
        done 3<&1
        umount_fs

+       # Omit udev daemon on halt to allow cryptsetup to do the close
+       OMITDIR=/run/sendsigs.omit.d
+       mkdir -p $OMITDIR
+       ps x | grep "systemd-udevd[ ]" | awk '{print $1}' > $OMITDIR/systemd-udevd
+
        log_action_end_msg 0
 }

เท่านี้ก็สามารถ shutdown เครื่องโดยไม่ค้างได้แล้ว (บางคนอาจเสนอให้ไปทำใน /etc/init.d/udev ไปเลย แต่ผมลองแล้ว มันทำให้ udev ไม่ start ตอนเปิดเครื่องเลยครับ บางที file system อาจยังไม่พร้อมในขั้นนั้นกระมัง?)

และเพื่อความแน่ใจ ผมจัดการ stop ดีมอนหลังจากที่ปิด crypto disk แล้วอีกชั้นหนึ่งด้วย ในฟังก์ชัน do_stop():

@@ -780,7 +785,11 @@ do_stop () {
                done 3<&1
        done

-       log_action_end_msg 0
+       # Kill udevd as we postponed it
+       OMITDIR=/run/sendsigs.omit.d
+       start-stop-daemon --stop -p $OMITDIR/systemd-udevd --user root --quiet --oknodo --retry 5
+
+       log_action_end_msg $?
 }

 # Convenience function to handle $VERBOSE

หมดไปครึ่งวัน แต่ตัดรำคาญเวลาเปิด-ปิดหรือรีบูตเครื่องไปได้เยอะเลยครับ และยังลดความเสี่ยงที่ file system จะเสียหายลงด้วย

ป้ายกำกับ: ,

29 มิถุนายน 2559

LibThai 0.1.25 : More on Thread-safety

LibThai 0.1.25 ออกแล้ว ความเปลี่ยนแปลงหลักของรุ่นนี้อยู่ที่เรื่อง API ใหม่ที่ thread-safe กว่าเดิม และเรื่องย่อย ๆ คือการแก้ปัญหาการคอมไพล์ด้วย GCC 6 และการปรับพจนานุกรมตัดคำตามปกติ

Thread Safety

ในรุ่น 0.1.23 ได้ทำเรื่อง thread safety ไปแล้วส่วนหนึ่ง จากประเด็นที่พบใน Pango เมื่อมีหลายเธรดพยายามเรียกฟังก์ชันตัดคำพร้อมกัน ทำให้เกิดการแย่งใช้ free list ดังที่เคยอธิบายไว้ใน blog เก่า แต่ก็ยังแก้ไม่หมดจดพอ ดังที่คุณ Mark Brown ได้รายงานมาใน กลุ่มเมล Thai Linux/FOSS developers ว่ายังเหลืออีกจุดหนึ่ง คือขณะเปิดพจนานุกรมเป็นการภายในในการเรียกครั้งแรก เพราะจะยังมีการแย่งกันเปิดพจนานุกรมจนเกิดออบเจกต์พจนานุกรมหลายชุด แม้สุดท้ายจะใช้งานแค่ชุดเดียวและโปรแกรมก็ไม่แครช แต่ออบเจกต์ชุดที่เหลือก็เปลืองเนื้อที่ในหน่วยความจำ และจะไม่ถูกทำลายเมื่อจบโปรแกรมอีกด้วย

วิธีแก้ปัญหาได้พัฒนามาเป็นขั้นเป็นตอนดังนี้ :-

  1. ใช้ mutex ขณะเปิดพจนานุกรม เพื่อให้มีเพียงเธรดเดียวที่เปิด เธรดที่เหลือแค่รอใช้ แต่ปัญหาคือ mutex ของแต่ละ OS จะเรียกไม่เหมือนกัน (บน Linux และ Unix-like OS ทั้งหลาย ใช้ POSIX thread ส่วนวินโดวส์ใช้ Mutex Object ของตัวเอง แม้จะมี pthreads-win32 เป็น wrapper ให้ใช้ แต่ก็ยังต้องสร้างระบบ build และทดสอบขึ้นมาอีก) การจะใช้ mutex ใน libthai จะต้องสร้าง layer ใหม่เพื่อให้ยังคงทำงานข้ามแพลตฟอร์มได้ เป็นงานที่ใหญ่พอสมควรเมื่อเทียบกับปัญหาที่แก้
  2. แยกฟังก์ชันเปิดพจนานุกรมออกมาต่างหาก เป็นแนวคิดที่คุณ Mark Brown ปิ๊งขึ้นมาระหว่างทำอีกประเด็นหนึ่ง คือ การอนุญาตให้ระบุแฟ้มพจนานุกรมที่จะโหลด สำหรับใช้ในกรณีที่ผู้ใช้ไม่ต้องการใช้พจนานุกรมมาตรฐานของ libthai ซึ่งเมื่อกำหนดฟังก์ชันนี้ขึ้นมาแล้ว ก็จะเกิดขั้นตอนใหม่เพิ่มขึ้นก่อนที่ผู้ใช้จะตัดคำ คือการเช็กและเปิดพจนานุกรม ซึ่งผู้ใช้สามารถเรียกใช้ใน critical region ที่มีการล็อคด้วย mutex เองได้ กลายเป็นการยิงปืนนัดเดียวได้นกสองตัว ดังที่คุณ Mark Brown ได้อธิบายมาใน อีกกระทู้หนึ่ง
  3. กำหนด type ThBrk ผมชอบแนวคิดของคุณ Mark Brown ที่ทำให้สามารถเลี่ยงการสร้าง portability layer เพิ่มได้ จึงได้ generalize ออกมาเป็น API ชุดใหม่ คือให้ผู้ใช้สร้างออบเจกต์ชนิด ThBrk (ซึ่งภายในเก็บพจนานุกรมที่เปิดแล้ว) ภายใต้การปกป้องด้วย mutex ในตอนต้น แล้วจึงเรียกฟังก์ชันตัดคำต่าง ๆ แบบขนานตามต้องการ ดังที่ผมได้แสดงความเห็นต่อมาในกระทู้ดังกล่าว คลาส ThBrk นี้ สามารถซ่อนรายละเอียดเพื่อรองรับ implementation แบบอื่นในอนาคตได้ถ้าต้องการ เช่น การใช้สถิติ tri-gram ฯลฯ แต่ในขณะนี้เราจะใช้พจนานุกรมเพียงอย่างเดียวก่อน

API นี้มีการเปลี่ยนชื่อไปมาเพื่อความชัดเจน ในแบบสุดท้าย สรุปว่าตัวอย่างการใช้งานจะเป็นแบบนี้ :-

  // mutex lock here
  ThBrk *brk = th_brk_new (dictpath);
  // mutex unlock here

  // works in parallel
  int pos[N];
  int res = th_brk_find_breaks (brk, str, pos, N);

  // mutex lock here
  th_brk_delete (brk);
  // mutex unlock here

API เดิมจะยังคงมีอยู่เพื่อ backward compatibility โดยจะเป็น wrapper ที่แอบเปิดพจนานุกรมเป็นการภายในเช่นเดิม (ซึ่งไม่ thread-safe) ก่อนเรียก API ชุดใหม่ แต่จะเริ่ม deprecate API เก่าตั้งแต่รุ่นนี้เป็นต้นไป เพื่อให้ผู้ใช้เปลี่ยนไปใช้ API ชุดใหม่แทน ดังนี้ :-

  • th_brk() ให้ใช้ th_brk_find_breaks() แทน
  • th_brk_line() ให้ใช้ th_brk_insert_breaks() แทน
  • th_wbrk() ให้ใช้ th_brk_wc_find_breaks() แทน
  • th_wbrk_line() ให้ใช้ th_brk_wc_insert_breaks() แทน

โดยผู้ใช้ต้องสร้างออบเจกต์ชนิด ThBrk ต่างหากเพื่อส่งให้ฟังก์ชันเหล่านี้ และทำลายเมื่อใช้งานเสร็จ

GCC 6

การปรับโค้ดอีกส่วนหนึ่งเป็นส่วนที่สะสมมาตั้งแต่ช่วงที่ Debian กำหนดให้ใช้ GCC 6 เป็นรุ่น default ที่จะใช้ใน Stretch และได้รับรายงานจากคุณ Martin Michlmayr ใน Debian #811690 ว่าแพกเกจ scim-thai มีปัญหาในการคอมไพล์ด้วย GCC 6 ซึ่งต้นเหตุอยู่ที่ header file หนึ่งของ libthai จึงปรับแก้เสีย

Word Break Dictionary

ก็เป็นปกติเหมือน libthai ทุกรุ่น ที่จะมีการปรับพจนานุกรมตัดคำตามคำที่พบใหม่อยู่เสมอ ที่ผ่านมาผมไม่เคยรีวิวเลยว่ามีคำแบบไหนบ้างที่เพิ่มเข้ามา คิดว่านำเสนอบ้างก็น่าจะดี

คำที่น่าสนใจที่เพิ่มมาในรุ่นนี้:

  • กรังด์ปรีซ์ : จากดราม่าวอลเลย์บอลหญิงไทย-ญี่ปุ่น
  • สไตรีน, โมโนเมอร์ : จากกรณีถกเถียงเรื่องการใช้กล่องโฟมบรรจุอาหาร
  • ทรัมป์ : จากการเมือง USA ในช่วงนี้
  • แอมเฟตามีน, ปุ๊น : จากข่าวการถอดยาบ้าออกจากบัญชียาเสพติดร้ายแรง
  • สเตอร์ลิง, เบรกซิท : จากข่าวประชามติ UK ถอนตัวจาก EU

และเช่นเคย ส่งเข้า Debian เรียบร้อยแล้วครับ

ป้ายกำกับ:

14 มิถุนายน 2559

Fonts-TLWG 0.6.3

Fonts-TLWG 0.6.3 ออกแล้ว เมื่ออาทิตย์ที่แล้ว แต่เพิ่งจะได้เขียน blog บันทึกหลังจากที่เตรียมแพกเกจเพื่ออัปโหลดในที่ต่าง ๆ เสร็จ คือที่ Debian และ CTAN

รุ่นนี้เป็นรุ่นแรกที่ ออกรุ่นจาก GitHub หลังจากที่ ประกาศย้าย repository ของ TLWG ไปที่ GitHub เมื่อเดือนที่แล้ว แต่ยังคงใช้ linux.thai.net เป็นที่ประกาศหลักตามเดิม เพื่อความต่อเนื่องกับรุ่นก่อน ๆ

ความเปลี่ยนแปลงหลักที่เกิดขึ้นในรุ่นนี้ คือการเปลี่ยนให้ฟอนต์ Loma เป็นฟอนต์ UI หลักแทน Waree อันเนื่องมาจาก รายงานบั๊กของพี่สัมพันธ์ พร้อม follow-up ว่าฟอนต์ Waree นั้นตัวสูงเกินไปจนถูกขริบในบางเว็บ เช่น Facebook แต่ Loma นั้นเตี้ยพอที่จะเล็ดรอดมาได้ (เหตุผลก็คือ Waree นั้นออกแบบเพื่อเตรียมเพิ่มอักษรไทยให้กับฟอนต์ DejaVu Sans จึงมี glyph ละตินของ DejaVu Sans ซึ่งตัวสูงอยู่แล้วเป็นตัวตั้ง ส่วน Loma นั้น เข้าใจว่าออกแบบโดยอิสระของตัวเอง) หลังจากทดสอบและฟังความเห็นของหลาย ๆ ท่านที่มาคอมเมนต์ ก็เห็นว่าควรเลื่อนอันดับของฟอนต์ Loma ขึ้นมาสูงกว่า Waree ในการเลือกฟอนต์ sans-serif ของ fontconfig

ในการเลื่อนขั้นนั้น ตามหลักแล้วควรจะทำได้ง่าย ๆ ด้วยการเปลี่ยนลำดับแฟ้ม config ของ fontconfig ให้ Loma ขึ้นก่อน Waree เช่น เปลี่ยนชื่อแฟ้ม /etc/fonts/conf.d/64-12-tlwg-loma.conf ในรุ่น 0.6.2 ให้เป็น 64-10-tlwg-loma.conf แต่ทำแค่นั้นไม่ได้ช่วยให้ Loma มาก่อน Waree ได้ เพราะยังมีกฎชุด synthetic อีกชุดหนึ่งเข้ามามีส่วนด้วย ดังผลลัพธ์หลังเปลี่ยนชื่อแฟ้มดังกล่าว ก็ยังคง match sans-serif ได้ Waree เช่นเดิม (ในรุ่น 0.6.2):

$ cd /etc/fonts/conf.d
$ sudo mv 64-12-tlwg-loma.conf 64-10-tlwg-loma.conf
$ fc-match sans-serif
Waree.otf: "Waree" "Regular"

สาเหตุคือฟอนต์ Waree มีการจำลองตัวเองเพื่อทดแทนฟอนต์ Tahoma ด้วยการห้อยชื่อตัวเองในลำดับต่อจาก Tahoma (ในไฟล์ 89-tlwg-waree-synthetic.conf) แต่ด้วยความที่ Tahoma ได้ถูกกำหนดให้เป็น preferred font ตัวหนึ่งของละติน (ในไฟล์ 60-latin.conf) ซึ่งมาก่อนภาษาอื่น ๆ การจำลอง Tahoma ของ Waree จึงทำให้ Waree กลายเป็นฟอนต์ละตินตัวหนึ่งที่มาก่อนภาษาอื่น ๆ ไปด้วย!

รายละเอียดทางเทคนิคของการตรวจสอบ ผู้ที่สนใจสามารถอ่านได้ที่ส่วนท้ายของ blog

ฉะนั้น การจะเลื่อนขั้น Loma ขึ้น จึงติดที่กฎชุด synthetic ของ Waree นี้

แล้วกฎ synthetic นี้มาจากไหน? มันเริ่มมาจากแนวคิดการจำลองฟอนต์ที่ผู้ใช้นิยมใช้ในเอกสารต่าง ๆ บนวินโดวส์ด้วยฟอนต์ที่มีในชุด Fonts-TLWG เช่น จำลอง Angsana ด้วย Kinnari, จำลอง Browallia ด้วย Garuda ฯลฯ ดังมีบันทึกไว้ใน blog เก่าเมื่อปี 2550 ซึ่งฟอนต์ต่าง ๆ ก็มีเค้าหน้าตาให้จำลองกันได้ จนกระทั่งมาเจอกรณีการใช้ฟอนต์ MS Sans Serif และ Tahoma ในเว็บ จึงได้พยายามทดแทนด้วยฟอนต์ Loma และ Waree ตามลำดับ ดังอ้างถึงใน blog เก่าเมื่อปี 2552 โดยที่ทั้งสองฟอนต์นี้ไม่ได้มีเค้าของฟอนต์ที่จำลองเลย เพียงแต่ต้องการอุดช่องว่างของการแสดงหน้าเว็บเท่านั้น

กฎนี้เคยถูกรายงานว่าสร้างปัญหาให้กับภาษาอื่น เพราะ Waree จะโผล่มาแทน Tahoma ทั้ง ๆ ที่ตัวใหญ่กว่า Tahoma (LP #434054) ทำให้เคยตัดกฏนี้ออกไปชั่วระยะหนึ่ง แต่ก็เพิ่มกลับเข้ามาพร้อมกับการตรวจสอบภาษาเพิ่มเติมเพื่อให้ apply กับภาษาไทยเท่านั้น (LP #539008) ดังบันทึกใน thaifonts-scalable 0.4.14 แต่ในเมื่อมันมารบกวนการจัดอันดับฟอนต์ sans-serif ไทยทั้งระบบ ก็เห็นควรว่าควรตัดกฎ synthetic นี้ออก แล้วปล่อยให้ฟอนต์ sans-serif ที่ได้อันดับสูงสุดมาอุด Tahoma เอา

เมื่อตัดกฎ synthetic ออก ฟอนต์ Loma ก็ได้อันดับสูงกว่า Waree แล้ว

อีกประเด็นหนึ่งที่พบระหว่างทดลองใช้ Loma เป็นฟอนต์ UI หลัก นอกเหนือจากขนาดที่เล็กลงถนัดตาที่ใช้เวลาสักพักก็จะชินแล้ว ก็มีเรื่องความกว้างของอักขระเว้นวรรค (space) ที่กว้างเกินพอดี และเมื่อเทียบกับฟอนต์ทั่วไปก็ยิ่งยืนยันได้ จึงได้ปรับลดความกว้างของอักขระเว้นวรรคให้แคบลง ซึ่งก็ทำให้ย่อหน้าภาษาอังกฤษไม่ดูโหรงเหรงจนเกินไป

ก่อนปรับ:

Loma spacing in 0.6.2

หลังปรับ:

Loma spacing in 0.6.3

ทั้งหมดนั้น เสิร์ฟถึงคุณแล้วในรุ่น 0.6.3 ครับ!


ต่อไปนี้เป็นวิธีตรวจสอบการ apply กฎของ fontconfig ด้วยตัวเอง ซึ่งทำให้เห็นว่ากฎ synthetic ใน 0.6.2 ทำให้ Waree ล้ำหน้าฟอนต์ไทยอื่น ๆ ได้อย่างไร

วิธีตรวจสอบตามที่ เอกสาร fontconfig ได้อธิบายไว้ คือกำหนดตัวแปร environment FC_DEBUG โดยในที่นี้เราสนใจการ edit match pattern ในกฎต่าง ๆ ก็กำหนดค่าเป็น 4 แล้วเรียก fc-match ดังนี้:

$ FC_DEBUG=4 fc-match sans-serif

มันจะพ่นข้อความแสดงการ edit match pattern ในแต่ละขั้นออกมายืดยาว ก็อาจจะ redirect ลงแฟ้มแล้วมาตรวจสอบดู ก็จะพบขั้นตอนที่น่าสนใจคือ:

...

FcConfigSubstitute test pattern any family Equal(ignore blanks) "sans-serif"
Substitute Edit family Prepend "Bitstream Vera Sans" Comma "DejaVu Sans" Comma "
Verdana" Comma "Arial" Comma "Albany AMT" Comma "Luxi Sans" Comma "Nimbus Sans L
" Comma "Helvetica" Comma "Lucida Sans Unicode" Comma "BPG Glaho International" 
Comma "Tahoma"

Prepend list before  "DejaVu Sans"(w) "DejaVu LGC Sans"(w) "DejaVu LGC Sans"(w) 
[marker] "sans-serif"(s) "sans-serif"(w)
Prepend list after  "DejaVu Sans"(w) "DejaVu LGC Sans"(w) "DejaVu LGC Sans"(w) "
Bitstream Vera Sans"(w) "DejaVu Sans"(w) "Verdana"(w) "Arial"(w) "Albany AMT"(w)
 "Luxi Sans"(w) "Nimbus Sans L"(w) "Helvetica"(w) "Lucida Sans Unicode"(w) "BPG 
Glaho International"(w) "Tahoma"(w) "sans-serif"(s) "sans-serif"(w)

...

กฎนี้มาจาก 60-latin.conf ของ fontconfig เอง ที่เพิ่ม prefer list ของ sans-serif โดยมี Tahoma พ่วงอยู่ด้วย

จากนั้น ก็มีการ edit match pattern ต่อ ๆ มา จนมาถึงตรงนี้:

...

FcConfigSubstitute test pattern any family Equal(ignore blanks) "sans-serif"
Substitute Edit family Prepend "Loma"

Prepend list before  "DejaVu Sans"(w) "DejaVu LGC Sans"(w) "DejaVu LGC Sans"(w) 
"Bitstream Vera Sans"(w) "DejaVu Sans"(w) "Verdana"(w) "Arial"(w) "Albany AMT"(w
) "Luxi Sans"(w) "Nimbus Sans L"(w) "Helvetica"(w) "Lucida Sans Unicode"(w) "BPG
 Glaho International"(w) "Tahoma"(w) [marker] "sans-serif"(s) "sans-serif"(w)
Prepend list after  "DejaVu Sans"(w) "DejaVu LGC Sans"(w) "DejaVu LGC Sans"(w) "
Bitstream Vera Sans"(w) "DejaVu Sans"(w) "Verdana"(w) "Arial"(w) "Albany AMT"(w)
 "Luxi Sans"(w) "Nimbus Sans L"(w) "Helvetica"(w) "Lucida Sans Unicode"(w) "BPG 
Glaho International"(w) "Tahoma"(w) "Loma"(w) "sans-serif"(s) "sans-serif"(w)

...

FcConfigSubstitute test pattern any family Equal(ignore blanks) "sans-serif"
Substitute Edit family Prepend "Waree"

Prepend list before  "DejaVu Sans"(w) "DejaVu LGC Sans"(w) "DejaVu LGC Sans"(w) 
"Bitstream Vera Sans"(w) "DejaVu Sans"(w) "Verdana"(w) "Arial"(w) "Albany AMT"(w
) "Luxi Sans"(w) "Nimbus Sans L"(w) "Helvetica"(w) "Lucida Sans Unicode"(w) "BPG
 Glaho International"(w) "Tahoma"(w) "Loma"(w) [marker] "sans-serif"(s) "sans-se
rif"(w)
Prepend list after  "DejaVu Sans"(w) "DejaVu LGC Sans"(w) "DejaVu LGC Sans"(w) "
Bitstream Vera Sans"(w) "DejaVu Sans"(w) "Verdana"(w) "Arial"(w) "Albany AMT"(w)
 "Luxi Sans"(w) "Nimbus Sans L"(w) "Helvetica"(w) "Lucida Sans Unicode"(w) "BPG 
Glaho International"(w) "Tahoma"(w) "Loma"(w) "Waree"(w) "sans-serif"(s) "sans-s
erif"(w)

...

ซึ่งก็ดูเรียบร้อยดี Loma น่าจะ match ก่อน Waree แล้ว จนกระทั่งมาถึงตรงนี้:

...

FcConfigSubstitute test pattern any lang Contains "th"
FcConfigSubstitute test pattern any family Equal "Tahoma"
Substitute Edit family Append "Waree"

Append list before  "DejaVu Sans"(w) "DejaVu LGC Sans"(w) "DejaVu LGC Sans"(w) "
Bitstream Vera Sans"(w) "DejaVu Sans"(w) "Verdana"(w) "Arial"(w) "Albany AMT"(w)
 "Luxi Sans"(w) "Nimbus Sans L"(w) "Helvetica"(w) "Lucida Sans Unicode"(w) "BPG 
Glaho International"(w) "Tahoma"(w) [marker] "Loma"(w) "Waree"(w) "Garuda"(w) "U
mpush"(w) "Laksaman"(w) "Meera"(w) "Khmer OS"(w) "Nachlieli"(w) "Lucida Sans Uni
code"(w) "Yudit Unicode"(w) "Kerkis"(w) "ArmNet Helvetica"(w) "Artsounk"(w) "BPG
 UTF8 M"(w) "Waree"(w) "Loma"(w) "Garuda"(w) "Umpush"(w) "Saysettha Unicode"(w) 
"JG Lao Old Arial"(w) "GF Zemen Unicode"(w) "Pigiarniq"(w) "B Davat"(w) "B Comps
et"(w) "Kacst-Qr"(w) "Urdu Nastaliq Unicode"(w) "Raghindi"(w) "Mukti Narrow"(w) 
"padmaa"(w) "Hapax Berbère"(w) "MS Gothic"(w) "UmePlus P Gothic"(w) "SimSun"(w) 
"PMingLiu"(w) "WenQuanYi Zen Hei"(w) "WenQuanYi Bitmap Song"(w) "AR PL ShanHeiSu
n Uni"(w) "AR PL New Sung"(w) "MgOpen Moderna"(w) "MgOpen Modata"(w) "MgOpen Cos
metica"(w) "VL Gothic"(w) "IPAMonaGothic"(w) "IPAGothic"(w) "Sazanami Gothic"(w)
 "Kochi Gothic"(w) "AR PL KaitiM GB"(w) "AR PL KaitiM Big5"(w) "AR PL ShanHeiSun
 Uni"(w) "AR PL SungtiL GB"(w) "AR PL Mingti2L Big5"(w) "MS ゴシック"(w) "ZYSo
ng18030"(w) "NanumGothic"(w) "UnDotum"(w) "Baekmuk Dotum"(w) "Baekmuk Gulim"(w) 
"KacstQura"(w) "Lohit Bengali"(w) "Lohit Gujarati"(w) "Lohit Hindi"(w) "Lohit Ma
rathi"(w) "Lohit Maithili"(w) "Lohit Kashmiri"(w) "Lohit Konkani"(w) "Lohit Nepa
li"(w) "Lohit Sindhi"(w) "Lohit Punjabi"(w) "Lohit Tamil"(w) "Meera"(w) "Lohit M
alayalam"(w) "Lohit Kannada"(w) "Lohit Telugu"(w) "Lohit Oriya"(w) "LKLUG"(w) "F
reeSans"(w) "Arial Unicode MS"(w) "Arial Unicode"(w) "Code2000"(w) "Code2001"(w)
 "sans-serif"(s) "Arundina Sans"(w) "Roya"(w) "Koodak"(w) "Terafik"(w) "sans-ser
if"(w) "sans-serif"(w) "sans-serif"(w) "sans-serif"(w) "sans-serif"(w) "sans-ser
if"(w) "sans-serif"(w) "sans-serif"(w)
Append list after  "DejaVu Sans"(w) "DejaVu LGC Sans"(w) "DejaVu LGC Sans"(w) "B
itstream Vera Sans"(w) "DejaVu Sans"(w) "Verdana"(w) "Arial"(w) "Albany AMT"(w) 
"Luxi Sans"(w) "Nimbus Sans L"(w) "Helvetica"(w) "Lucida Sans Unicode"(w) "BPG G
laho International"(w) "Tahoma"(w) "Waree"(w) "Loma"(w) "Waree"(w) "Garuda"(w) "
Umpush"(w) "Laksaman"(w) "Meera"(w) "Khmer OS"(w) "Nachlieli"(w) "Lucida Sans Un
icode"(w) "Yudit Unicode"(w) "Kerkis"(w) "ArmNet Helvetica"(w) "Artsounk"(w) "BP
G UTF8 M"(w) "Waree"(w) "Loma"(w) "Garuda"(w) "Umpush"(w) "Saysettha Unicode"(w)
 "JG Lao Old Arial"(w) "GF Zemen Unicode"(w) "Pigiarniq"(w) "B Davat"(w) "B Comp
set"(w) "Kacst-Qr"(w) "Urdu Nastaliq Unicode"(w) "Raghindi"(w) "Mukti Narrow"(w)
 "padmaa"(w) "Hapax Berbère"(w) "MS Gothic"(w) "UmePlus P Gothic"(w) "SimSun"(w)
 "PMingLiu"(w) "WenQuanYi Zen Hei"(w) "WenQuanYi Bitmap Song"(w) "AR PL ShanHeiS
un Uni"(w) "AR PL New Sung"(w) "MgOpen Moderna"(w) "MgOpen Modata"(w) "MgOpen Co
smetica"(w) "VL Gothic"(w) "IPAMonaGothic"(w) "IPAGothic"(w) "Sazanami Gothic"(w
) "Kochi Gothic"(w) "AR PL KaitiM GB"(w) "AR PL KaitiM Big5"(w) "AR PL ShanHeiSu
n Uni"(w) "AR PL SungtiL GB"(w) "AR PL Mingti2L Big5"(w) "MS ゴシック"(w) "ZYS
ong18030"(w) "NanumGothic"(w) "UnDotum"(w) "Baekmuk Dotum"(w) "Baekmuk Gulim"(w)
 "KacstQura"(w) "Lohit Bengali"(w) "Lohit Gujarati"(w) "Lohit Hindi"(w) "Lohit M
arathi"(w) "Lohit Maithili"(w) "Lohit Kashmiri"(w) "Lohit Konkani"(w) "Lohit Nep
ali"(w) "Lohit Sindhi"(w) "Lohit Punjabi"(w) "Lohit Tamil"(w) "Meera"(w) "Lohit 
Malayalam"(w) "Lohit Kannada"(w) "Lohit Telugu"(w) "Lohit Oriya"(w) "LKLUG"(w) "
FreeSans"(w) "Arial Unicode MS"(w) "Arial Unicode"(w) "Code2000"(w) "Code2001"(w
) "sans-serif"(s) "Arundina Sans"(w) "Roya"(w) "Koodak"(w) "Terafik"(w) "sans-se
rif"(w) "sans-serif"(w) "sans-serif"(w) "sans-serif"(w) "sans-serif"(w) "sans-se
rif"(w) "sans-serif"(w) "sans-serif"(w)

...

กลายเป็นว่า Waree กลับมาแซงหน้า Loma อีกครั้งหลังจากกฎใน 89-tlwg-waree-synthetic.conf ผลก็คือ ไม่ว่าฟอนต์ไทยตัวไหนจะพยายามลิสต์ตัวเองขึ้นก่อนอย่างไรก็ตาม ก็จะแพ้ฟอนต์ที่ fallback ให้ Tahoma อยู่วันยังค่ำ และการสังเคราะห์ฟอนต์ Tahoma ด้วย Waree ก็ทำให้ Waree ไม่ยอมลงให้กับใคร!

ป้ายกำกับ: ,

01 พฤษภาคม 2559

From C++ to Python (2)

ใน blog ที่แล้ว ผมได้พูดถึงประเด็นต่าง ๆ ที่ผมสะดุดเมื่อเรียนภาษาไพธอนด้วยพื้นฐานความรู้ C/C++ ที่มี ซึ่งเป็นการปรับโหมดคิดพื้นฐานให้เพียงพอสำหรับใช้เขียนไพธอนได้ ใน blog นี้จะขอเขียนส่วนที่เป็น กำไร ที่ได้จากภาษาไพธอนบ้าง

Container สำเร็จรูป

ภาษาไพธอนมาตรฐานมาพร้อมกับ container สำเร็จรูป คือ list, dictionary และ set โดยสามารถเก็บข้อมูลหลายชนิดปนกันได้ตามธรรมชาติของภาษา dynamic type (heterogeneous list ที่ต้องอาศัย polymorphism หรือ generic programming ใน C++ กลายเป็นเรื่องที่แสนธรรมดาเมื่อมาเขียนไพธอน) การมี container สำเร็จรูปทำให้เขียนโปรแกรมได้สะดวกขึ้นมาก

list (และ tuple ที่เป็น immutable list) นั้น มีแนวคิดเหมือนลิสต์ของภาษา Lisp ที่สามารถบรรจุข้อมูลหลากชนิดคละกันได้ รวมทั้งเก็บลิสต์ในลิสต์ กลายเป็น tree ก็ยังได้ (ยังมีอิทธิพลของภาษา Lisp ในไพธอนอีกอย่าง คือ lambda expression) แต่ implement ด้วย dynamic array เพื่อรองรับ syntax ในการเข้าถึงสมาชิกในแบบแอร์เรย์อย่างมีประสิทธิภาพ แลกกับ cost ในการเพิ่ม/ลบสมาชิกเล็กน้อย list ของไพธอนจึงทำให้สามารถทำงานกับ collection ของข้อมูลได้สะดวกโดยไม่ต้องคิดเรื่องวิธีจองหน่วยความจำ

dictionary ทำให้การใช้งาน associative array ที่พบบ่อยในโปรแกรมต่าง ๆ กลายเป็นเรื่องง่าย (กลไกภายในคือ hash table) แม้แต่ตัว interpreter ของไพธอนเองก็ยังใช้ dictionary เป็นกลไกในการทำงานหลายส่วน เช่น ใช้ในการเก็บ attribute และ method ของออบเจกต์ต่าง ๆ แบบ dynamic, การทำ symbol table ของโปรแกรม ฯลฯ

set เป็นการ implement แนวคิดของ ทฤษฎีเซ็ต ในทางคณิตศาสตร์นั่นเอง การใช้เซ็ตในโปรแกรมได้ ทำให้สามารถเขียนโปรแกรมได้ใกล้เคียงกับนิพจน์คณิตศาสตร์มากขึ้น

การมีเครื่องมือแบบนี้ พร้อม syntax ที่เรียบง่ายในการเข้าถึงในระดับตัวภาษาเอง ทำให้เขียนโปรแกรมได้สั้นกระชับ

else ในที่ต่าง ๆ

นอกจาก else ใน if แล้ว ไพธอนยังมี else ในลูป while, for, และใน exception handling ด้วย ซึ่งคนที่เขียนโปรแกรม C/C++ มาเยอะหน่อยอาจเคยพบกรณีที่ else เหล่านี้ช่วยลดขั้นตอนลงได้

สมมุติว่ามีการค้นหาสมาชิกในลิสต์ที่สอดคล้องกับเงื่อนไขที่กำหนด

for (const Elm* p = students.first(); p; p = p->next()) {
  log_visited (p);
  if (p->id() == id) {
    report_matched (p);
    break;
  }
  mark_unmatched (p);
}

if (!p) {
  // search exhausted
  report_no_match();
}

เราไม่จำเป็นต้องเช็กค่า p อีกครั้งหลังจบลูปถ้าเราใช้ else หลัง for แบบนี้ในไพธอน:

for s in students:
  log_visited (s)
  if (s.id == id):
    report_matched (s)
    break
  mark_unmatched (s)
else:
  # search exhausted
  report_no_match()

หรือจะเป็นโปรแกรมให้ผู้ใช้ทายตัวเลข โดยให้ผู้ใช้หยุดทายได้ด้วยการป้อนค่า 0 หรือเลขลบ:

guess = 0;
while (guess != secret) {
  std::cout << "Guess the number: ";
  std::cin >> guess;

  if (guess <= 0) {
    std::cout << "Sorry that you're giving up!" << std::endl;
    break;
  }

  if (guess > secret)
    std::cout << "The number is too large." << std::endl;
  else if (guess < secret)
    std::cout << "The number is too small." << std::endl;
}

if (guess == secret)
  std::cout << "Congratulations. You made it!" << std::endl;

ด้วย else ในไพธอน คุณก็ไม่ต้องเช็กค่า guess ซ้ำหลังจบลูป:

guess = 0
while guess != secret:
  guess = int (input ("Guess the number: "))

  if guess <= 0:
    print ("Sorry that you're giving up!")
    break

  if guess > secret:
    print ("The number is too large.")
  elif guess < secret:
    print ("The number is too small.")
else: 
  print ("Congratulations. You made it!")

โค้ดแบบนี้ผมเจอค่อนข้างบ่อยใน C/C++ บางทีคิด ๆ เหมือนกันว่าถ้าใช้ goto แทน break ซะก็อาจไม่ต้องมาเช็กซ้ำ พอมาเจอ else ของลูปในไพธอนก็เข้าใจได้ทันที

List Comprehension

list comprehension เป็นสิ่งที่ pythonic เอามาก ๆ ทำให้โค้ดกระชับและดูคล้ายนิพจน์คณิตศาสตร์

เช่น ถ้าต้องการหารายการข้อมูลในลิสต์ data ที่สูงกว่าค่าเฉลี่ย:

avg = sum(data)/len(data)
print([x for x in data if x > avg])

หรือแม้กระทั่งจะหาจำนวนเฉพาะทั้งหมดที่น้อยกว่าจำนวนที่กำหนด:

from math import sqrt
def primes(n):
  sqrt_n = int(sqrt(n))
  no_primes = {j for i in range(2, sqrt_n) for j in range(i*2, n, i)}
  return [i for i in range(2, n) if i not in no_primes]

พี่จะสั้นไปไหนครับ!

ในบทที่ว่าด้วย Lambda operator, map, filter, reduce บอกไว้ที่ส่วนต้นว่า Guido van Rossum ผู้สร้างและดูแลภาษาไพธอนได้แสดงความประสงค์ที่จะ ตัด lambda, map, filter, reduce ออกใน Python 3 เพราะ list comprehension ทำสิ่งเดียวกันได้ชัดเจนและเข้าใจง่ายกว่า แต่สุดท้ายก็ทนแรงต้านจากผู้นิยม Lisp, Scheme ไม่ไหว จำเป็นต้องคงไว้ ตัดออกเฉพาะ reduce() โดยย้ายไปไว้ในมอดูล functools

เทียบกันแล้ว list comprehension ถอดแบบมาจาก set-builder notation ในทฤษฎีเซ็ต ส่วน lambda นั้น ถอดแบบมาจาก lambda calculus เห็นได้ชัดว่าทฤษฎีเซ็ตเป็นที่คุ้นเคยและเข้าใจง่ายกว่า สิ่งที่ lambda ทำได้มากกว่า list comprehension ก็คือ reduce() ซึ่งในความเห็นของผู้สร้างไพธอนแล้ว ทำให้โค้ดซับซ้อนเกินไป ยอมเขียนเป็นลูปเพื่อความชัดเจนเสียจะดีกว่า

ไหนลองเขียนด้วย lambda ดูซิ:

หาข้อมูลที่สูงกว่าค่าเฉลี่ย:

avg = sum(data)/len(data)
print(list(filter(lambda x : x > avg, data)))

หาจำนวนเฉพาะ:

from math import sqrt
from functools import reduce
def primes(n):
  sqrt_n = int(sqrt(n))
  np_series = list(map(lambda i : set(range(i*2, n, i)), list(range(2,sqrt_n))))
  np_set = reduce(lambda a, b : a | b, np_series)
  return list(filter(lambda i : i not in np_set, list(range(2,n))))

จะเห็นว่า lambda ยาวและเข้าใจยากกว่า list comprehension

Generator

ลูป for ในไพธอนจะไม่มีรูปแบบการใช้ตัวนับเหมือนภาษาทั่วไป แต่จะใช้ iterator ล้วน ๆ คือเป็นลูป foreach นั่นเอง

เช่น ลูปหาผลรวมของกำลังสองของจำนวนเต็มบวก n ตัวแรกที่เขียนในภาษา C++ อาจเป็นแบบนี้:

int sum_sq (int n)
{
  int sum = 0;
  for (int i = 1; i <= n; i++)
    sum += i*i;
  return sum;
}

แต่ลูป for ในไพธอนจะใช้ฟังก์ชัน range() สร้าง iterator สำหรับไล่เรียง:

def sum_sq (n):
  sum = 0
  for i in range (1, n+1):
    sum += i*i
  return sum

หรือจะให้ pythonic จริง ๆ ก็ใช้ list comprehension:

def sum_sq (n):
  return sum([i*i for i in range (1, n+1)])

iterable object ต่าง ๆ เช่น list, tuple, dictionary, set สามารถใช้เป็น iterator ได้ทันที นอกจากนี้ ยังสามารถสร้าง iterator ขึ้นเองได้ โดยทำตามโพรโทคอลที่กำหนด (สร้างคลาสที่มีเมธอด __iter__(), next() โดย next() คืนค่าตัววิ่งแต่ละขั้น และ raise StopIteration exception เมื่อวิ่งสุดแล้ว) แต่เพื่ออำนวยความสะดวกยิ่งขึ้น ไพธอนได้บัญญัติสิ่งที่เรียกว่า generator ที่ใช้วิ่งลูปได้เหมือน iterator แต่เขียนง่ายกว่า

ตัวอย่างเช่น ถ้าจะเขียน generator สำหรับไล่ลำดับ Fibonacci:

def fibo (n):
  a, b = 0, 1
  for i in range(n):
    yield a
    a, b = b, a + b

(yield ทำหน้าที่คล้าย return สำหรับแต่ละรอบ และรอบต่อไปก็จะเริ่มทำงานต่อจากบรรทัดที่ yield ไว้)

จากนั้นก็สามารถไล่ลำดับ Fibonacci ได้ตามต้องการ:

>>> list(fibo(10))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
>>> for x in fibo(5):
...   print (x)
... 
0
1
1
2
3
>>> [x for x in fibo(10) if x % 2 == 0]
[0, 2, 8, 34]

การมี construct แบบ generator ก็ทำให้มีความยืดหยุ่นของการเขียน iterator เช่น สามารถเขียน generator แบบ recursive ได้ หรือกระทั่งเขียน generator ซ้อน generator ได้ อ่านเพิ่มเติม

เมื่อมองย้อนกลับไปถึงตอนแรกที่เราพบว่าไพธอนมีแต่ลูป foreach เท่านั้น ก็ไม่ได้ทำให้ความสามารถด้อยไปกว่าภาษาที่มีลูปตัวนับแต่อย่างใด (ในเมื่อมีฟังก์ชัน range()) แต่กลับมีความยืดหยุ่นสูงมากในการวนลูปที่ซับซ้อน

Decorator

ข้อนี้ไม่แน่ใจนักว่าชอบหรือเปล่า แต่ก็เป็นสิ่งที่น่าจะมีประโยชน์ในบางโอกาส คือสิ่งที่ไพธอนเรียกว่า decorator ซึ่งหลังจากที่ทำความเข้าใจแล้ว อยากจะเรียกว่า wrapper มากกว่า

สมมุติว่าเราต้องการนับจำนวนการเรียกฟังก์ชันต่าง ๆ ในโปรแกรมของเรา เราสามารถเขียน wrapper function มาดักการเรียกของผู้ใช้แล้วแอบเพิ่มตัวนับก่อนเรียกฟังก์ชันตัวจริง และไพธอนมีวิธีการสร้าง wrapper ที่ว่านี้อย่างแนบเนียน

def call_counter (func):
  def wrapper (*args, **kwargs):
    wrapper.calls += 1
    return func (*args, **kwargs)
  wrapper.calls = 0
  wrapper.__name__ = func.__name__
  return wrapper

@call_counter
def square (x):
  return x*x

print (square.calls)
for i in range (10):
  print (square (i))
print (square.calls)

บรรทัด @call_counter คือ syntax ของไพธอนในการ decorate ฟังก์ชัน โดยโค้ดนี้:

@call_counter
def square (x):
  return x*x

มีความหมายเทียบเท่ากับ:

def square (x):
  return x*x
square = call_counter (square)

กล่าวคือ เป็นการส่งออบเจกต์ของฟังก์ชัน square() ให้กับฟังก์ชัน call_counter() แล้ว call_counter() คืนค่า wrapper() ซึ่งเป็นฟังก์ชันภายในมา จากนั้น ก็ใช้ค่าที่คืนมานี้ assign ค่าทับลงไปใน symbol square() เสีย ทำให้การเรียก square() หลังจากนี้ไปจะเป็นการเรียกตัวฟังก์ชัน wrapper() ที่ได้แอบเพิ่มตัวนับก่อนเรียกฟังก์ชัน square() ตัวจริงที่ได้ส่งมาก่อนหน้านี้ในพารามิเตอร์ชื่อ func

(คำอธิบายค่อนข้างซับซ้อนวนเวียนสักหน่อย หากงงก็ขอแนะนำให้อ่านรายละเอียดจาก บทเรียน นอกจากนี้ยังมี blog ที่ artima และ blog ของ Simeon Franklin ที่ให้คำอธิบายอย่างละเอียด)

อีกวิธีหนึ่งคือเขียน wrapper เป็นคลาสที่ callable ซึ่งดูสะอาดกว่า:

class call_counter:

  def __init__ (self, func):
    self.func = func
    self.calls = 0

  def __call__ (self, *args, **kwargs):
    self.calls += 1
    return self.func (*args, **kwargs)

ประโยชน์ที่พอมองเห็นได้คือ ไพธอนมีวิธีสร้าง wrapper ที่แนบเนียน wrapper มีประโยชน์ที่ไหนก็ใช้ได้ที่นั่น (เช่น การ cache ผลการคำนวณครั้งก่อน ๆ ของฟังก์ชัน, การเพิ่มการตรวจสอบอาร์กิวเมนต์ ฯลฯ)

Library

นอกจากตัวภาษาเองแล้ว ไพธอนยังมาพร้อมกับไลบรารีมาตรฐานอีกเพียบ ซึ่งผมคงต้องศึกษาเพิ่มเติมไปเรื่อย ๆ เท่าที่ได้ผ่านมาก็คือเรื่องการใช้ regular expression

ดูยังมีอะไรอีกเยอะให้ศึกษาข้างหน้า ที่สำคัญคือการฝึกฝนให้เกิด pythonic way ในการเขียนโค้ด และการใช้แพกเกจต่าง ๆ ให้เหมาะกับงาน

Update (2016-05-04): แก้เนื้อหาเรื่อง implementation ของ list หลังจากที่คุณวีร์ทักท้วงมาใน facebook ว่า list ของไพธอน implement ด้วย array ไม่ใช่ linked list เหมือนใน Lisp

ป้ายกำกับ: , ,

27 เมษายน 2559

From C++ to Python (1)

ภาษาไพธอน เป็นภาษาที่ผมอยากหาโอกาสเรียนมานานแล้ว แต่ด้วยงานส่วนใหญ่ที่ผมทำยังวนเวียนอยู่แถว ๆ ระบบระดับล่างซึ่งใช้ C/C++ เป็นหลัก ก็เลยยังปล่อยมือมาเรียนภาษาอื่นไม่ถนัดถนี่ จนกระทั่งระบบภาษาไทยเริ่มจะอยู่ตัวแล้ว ถึงเริ่มเรียนภาษาอื่น ๆ เพื่อเพิ่มโอกาสในการทำงานแขนงอื่นบ้าง

ในการเรียนครั้งนี้ ผมได้อาศัยบทเรียนจาก python-course.eu (และไปอ่านทบทวนกับ บทเรียนที่ debianclub ที่คุณวิทยาเคยลงไว้)

บทเรียนที่ python-course.eu นับว่าเหมาะกับคนที่มีพื้นฐาน C/C++ มาแล้วมาก เพราะเขาอธิบายบนพื้นฐานของคนเคยเขียนโปรแกรมมาแล้ว ไม่ใช่เริ่มต้นจากศูนย์ ยกตัวอย่างเปรียบเทียบกับ C/C++ และเน้นประเด็นที่เป็นหลุมพรางของผู้ที่ย้ายมาจาก C/C++

เท่าที่ได้ตั้งโจทย์ฝึกหัดให้กับตัวเอง ด้วยการทดลองพอร์ตโค้ดภาษา C/C++ ของตัวเองที่เคยเขียนไว้ให้เป็นไพธอน พร้อมกับทำ unit test ไปด้วย ทำให้ได้พบเจอประเด็นต่าง ๆ ที่โปรแกรมเมอร์ภาษา C/C++ จะสะดุด ผมเองก็พบว่าต้องก้าวข้ามสิ่งเหล่านี้ถึงจะเริ่มจับทางไพธอนได้ จึงเขียนบันทึกไว้สักหน่อย

Indentation

ข้อนี้ดูเหมือนเป็นเรื่องเล็ก แต่กลับเป็นสาเหตุหลักข้อหนึ่งที่ทำให้ผมยี้ไพธอนก่อนหน้านี้ เพราะโปรแกรมเมอร์ภาษา C/C++ จะคุ้นกับ free-form syntax และพบเจอการ indent แบบตามใจฉันมาแล้วมากมาย บางคนใช้ tab บางคนใช้ space 2, 4 หรือ 8 ช่อง บางคนใช้ปนกันทั้ง tab ทั้ง space ซึ่งซอร์สจะเละทันทีเมื่อมีการเปลี่ยนขนาดของ tab stop นี่ยังไม่นับคนที่ไม่สนใจ style ใด ๆ ทั้งสิ้นอีกนะ แต่ไม่ว่าอย่างไรโปรแกรมก็ยังคอมไพล์ผ่าน แล้วถ้ามันกลายเป็นการกำหนด syntax ของภาษา มันจะเละเทะขนาดไหน

เมื่อพยายามนึกถึงตัวอย่างอื่นที่กำหนดอะไรทำนองนี้ ผมก็นึกถึง Makefile ที่ใช้ tab ในการกำหนดขอบเขตของ rule แต่มันก็แค่ระดับเดียว ไม่ได้ซ้อนกันหลายชั้น จึงดูไม่มีอะไรมาก และอีกตัวอย่างหนึ่งคือ Fortran 77 ที่เคยเขียนในสมัยเรียน ซึ่งอาศัยตำแหน่งคอลัมน์เป็นส่วนหนึ่งของ syntax อันเป็นมรดกตกทอดมาจากสมัยใช้บัตรเจาะรู (ตอนที่เรียน Fortran 77 รู้สึกอึดอัดตรงนี้มาก เพราะตอนนั้นผ่านภาษา Pascal มาแล้ว แม้แต่ภาษา BASIC [AppleSoft, GW] ที่ว่าไม่ค่อยมีโครงสร้างก็ยังไม่ทำอะไรโลว์เทคเยี่ยงนี้) แต่ละอย่างที่นึกถึงก็ไม่ได้ชวนพิสมัยเอาเสียเลย

นั่นคือ mindset ของผมก่อนเรียน แต่ คำอธิบาย ในบทเรียนก็ทำให้ผมเข้าใจและยอมรับ ทำให้เรียนไพธอนต่อไปได้อย่างผ่อนคลาย มันคือการกำหนดขอบเขตของบล็อคคำสั่งโดยเปลี่ยนจาก style ให้เป็น syntax เสีย คนที่เขียนโปรแกรมภาษา C/C++ โดยเคร่งครัดกับ style อยู่แล้วจึงยอมรับตรงนี้ได้ไม่ยาก ส่วนประเด็นเรื่องการใช้ tab กับ space ปนกัน ไพธอนก็มีข้อกำหนดที่ชัดเจนที่ทำให้แยกแยะออกจากกันได้ คือถือเป็น indentation คนละระดับไปเลย ไม่นับปนกัน

มันคือการกำจัดวิจิกิจฉานิวรณ์ครับ ผ่านตรงนี้ไปได้ก็ลื่นไหลขึ้นเยอะ

Public, Protected, Private

ภาษา C++ จะมีการกำหนด access specifier สำหรับ member ต่าง ๆ ของคลาสเป็น public, protected, private จากนั้น โปรแกรมเมอร์แต่ละคนก็จะมีวิธีการต่าง ๆ ในการตั้งชื่อ member ให้สามารถแยกแยะ access ได้ บางคนใช้ตัวพิมพ์เล็ก-พิมพ์ใหญ่ต้นชื่อ บางคนใช้ prefix บางคนใช้ suffix บางคนไม่แยกเลย (อันนี้ยุ่ง)

แต่ไพธอนไม่มี access specifier แต่จะใช้การตั้งชื่อแยกแยะทันที โดยชื่อปกติจะถือเป็น public ชื่อที่ขึ้นต้นด้วย _ ถือเป็น protected และชื่อที่ขึ้นต้นด้วย __ ถือเป็น private นับว่าเป็นการเปลี่ยน style ให้เป็น syntax อีกหนึ่งเรื่อง

Dynamic Type & Scope

การย้ายจากภาษาที่เป็น static typing มาเป็น dynamic typing จะต้องปรับโหมดความคิดสักหน่อย ซึ่งพื้นฐานภาษา BASIC สมัยเก่า บวกกับความรู้จากวิชา Compilers & Programming Languages พอช่วยได้ ไม่เป็นปัญหาสำหรับผมนัก สิ่งที่ต้องปรับเปลี่ยนแนวคิดก็เช่น:

  • ชนิดของตัวแปรเป็น dynamic ไม่ใช่ตายตัวเปลี่ยนชนิดไม่ได้เหมือนในภาษา static type ทั้งหลาย ถึงตัวแปรหนึ่งจะเก็บค่า integer อยู่ คุณจะ assign สตริงหรือลิสต์ให้มันใหม่ก็ยังได้
    >>> x = 1
    >>> print(x)
    1
    >>> x = "Hello!"
    >>> print(x)
    Hello!
    
  • scope ของตัวแปรเป็นแบบ dynamic ฟังก์ชันหนึ่ง ๆ ทำงานแต่ละครั้งอาจเข้าถึงตัวแปรได้ไม่เหมือนกัน ขึ้นอยู่กับ state ของทั้งโปรแกรมในขณะนั้น
    >>> def f():
    ...   print(s)
    ... 
    >>> s = "Hello"
    >>> f()
    Hello
    >>> s = 2
    >>> f()
    2
    
    แต่ dynamic scope นี้ก็ถูกขี่ทับด้วย block scope ได้ ถ้ามีการกำหนดตัวแปรโลคอลในชื่อเดียวกัน
    >>> def g():
    ...   s = "Bye"
    ...   print(s)
    ... 
    >>> s = "Hello"
    >>> g()
    Bye
    >>> print(s)
    Hello
    
    อ่านเพิ่มเติม

Call by Value หรือ Call by Reference?

ภาษาไพธอนไม่มีพอยน์เตอร์เหมือน C/C++ แต่กลไกภายในกลับใช้พอยน์เตอร์เต็มไปหมด แม้แต่ตัวแปรกับค่าของมันก็เชื่อมกันด้วยพอยน์เตอร์ การ assign ตัวแปรก็เป็นการ assign พอยน์เตอร์ไปยังออบเจกต์ที่เป็นค่า ดังสามารถตรวจสอบได้ดังนี้:

>>> x = 1
>>> y = x
>>> id(x)
10861696
>>> id(y)
10861696

จะเห็นว่า หลัง assignment แล้ว x กับ y ชี้ไปยังออบเจกต์เดียวกัน ไม่ใช่แค่มีค่าเท่ากัน! อย่างไรก็ดี ถ้าเรา assign ค่าใหม่ให้กับ y จะไม่ได้ทำให้ค่าของ x เปลี่ยน แต่เป็นการกำหนดให้ y ชี้ไปยังออบเจกต์ใหม่:

>>> y = 2
>>> id(y)
10861728
>>> x
1

ก็น่าจะปลอดภัยดี แถมยังเป็นการประหยัดหน่วยความจำด้วยถ้าค่าเป็นออบเจกต์ใหญ่ ๆ ที่ไม่ใช่ integer เรื่องประหยัดน่ะใช่ แต่แน่ใจหรือเรื่องความปลอดภัย?

>>> p = [1, 2, 3]
>>> q = p
>>> q[1] = 'x'
>>> p
[1, 'x', 3]

จะเห็นว่าลิสต์ p เปลี่ยนตาม q หลัง assignment ทั้งนี้เพราะทั้ง p และ q ชี้ไปยังออบเจกต์เดียวกันตลอดเวลา และการ assign q[1] ก็เป็นการเปลี่ยนพอยน์เตอร์ q[1] จากที่ชี้ไปยัง integer 2 ให้ชี้ไปยัง string 'x' แทน แต่ในเมื่อ q ชี้ไปยังออบเจกต์เดียวกันกับ p การเปลี่ยนพอยน์เตอร์ q[1] จึงเป็นการเปลี่ยนพอยน์เตอร์ p[1] ด้วย!

พฤติกรรมแบบนี้ เกิดกับออบเจกต์ที่เป็น complex data structure ทั้งหมด ไม่ว่าจะเป็น list, dictionary หรือ class instance ซึ่งหากอธิบายในภาษาของ C/C++ ด้วยคำว่า พอยน์เตอร์ (จากชื่อตัวแปรไปยังค่า) ก็จะสามารถทำความเข้าใจได้ รวมถึงประโยชน์ของ deepcopy ด้วย

ความสนุกเกิดขึ้นเมื่อเราส่งอาร์กิวเมนต์ให้กับฟังก์ชัน

แบบนี้พฤติกรรมจะเหมือน call by value:

>>> def f(x):
...   x = x + [1]
... 
>>> p = [1, 2, 3]
>>> f(p)
>>> p
[1, 2, 3]

แต่แบบนี้กลับเหมือน call by reference:

>>> def g(x):
...   x += [1]
... 
>>> p = [1, 2, 3]
>>> g(p)
>>> p
[1, 2, 3, 1]

คำอธิบายก็คือ x ซึ่งเป็นพารามิเตอร์ของ f() และ g() นั้น จะก็อปปี้พอยน์เตอร์ไปยังค่าของ p มาทั้งคู่ จากนั้น x ใน f() ถูก assign ให้ชี้ไปยังออบเจกต์ใหม่ที่เป็นผลลัพธ์ของการบวก x กับลิสต์ [1] ในขณะที่ x ใน g() ถูกเพิ่มอิลิเมนต์ใหม่ต่อท้ายโดยตรงในออบเจกต์เดิม ซึ่งเป็นออบเจกต์เดียวกับที่ p ของผู้เรียกชี้อยู่ ผลจึงเป็นการเปลี่ยนค่าของ p ของผู้เรียกไปด้วย

คำถามที่ว่า การเรียกฟังก์ชันของไพธอนเป็น call by value หรือ call by reference จึงตอบได้แค่ว่า ไม่ใช่ทั้งสองอย่าง ผู้ที่ย้ายมากจากภาษา C/C++ ต้องระวังให้ดี ภาษาไพธอนไม่มีพอยน์เตอร์ก็จริง แต่เวลาเขียนไพธอนให้นึกถึงพอยน์เตอร์เข้าไว้

Function Overloading?

ฟังก์ชันในภาษา C++ สามารถโอเวอร์โหลดเพื่อรับพารามิเตอร์หลายแบบได้ ถึงแม้มันจะเพิ่มความยุ่งยากในการลิงก์จากโค้ดภายนอกอันเนื่องมาจาก symbol mangling แต่มันก็ไม่ได้สร้างขึ้นมาเท่ ๆ มี use case ที่ได้ประโยชน์จากการโอเวอร์โหลดฟังก์ชัน เช่น:

  • ช่วยให้สร้าง constructor หลายแบบได้
  • ช่วยในการ overload operator เพื่อรับ operand หลายชนิดได้

แต่ไพธอนไม่สามารถโอเวอร์โหลดฟังก์ชันได้ มีให้อย่างมากก็แค่การใช้ default argument แต่ถ้ากำหนดฟังก์ชันชื่อเดียวกันหลายครั้ง มันก็จะเอาอันหลังสุดเท่านั้น ถือว่าทับอันแรก ๆ ไป

ด้วยความเป็น dynamic typing ของภาษาไพธอน ทำให้พอเข้าใจได้ว่าการโอเวอร์โหลดฟังก์ชันสามารถทำให้เกิดความยุ่งยากได้ กรณีทั่วไปเราอาจเลี่ยงได้ด้วยการตั้งชื่อฟังก์ชันหลบกันเสีย แต่สำหรับกรณีของ magic methods ทั้งหลายของคลาส เราไม่สามารถตั้งชื่อหลบได้ ซึ่งกรณีของ constructor และ operator overloading ก็เข้าข่าย magic methods ทั้งสิ้น

แล้วจะจัดการกับ use case ข้างต้นได้อย่างไร?

การโอเวอร์โหลด constructor

กรณีที่สามารถใช้ default argument ได้ ก็ใช้ default argument เช่น:

class Time:
  def __init__ (self, h=0, m=0, s=0):
    self.h, self.m, self.s = h, m, s

t1 = Time()
t2 = Time(8)
t3 = Time(8, 20)
t4 = Time(8, 20, 45)

หากโอเวอร์โหลดโดยใช้อาร์กิวเมนต์ต่างชนิดกัน ก็ไม่สามารถใช้ default argument ได้ ก็อาจใช้วิธีพิเศษ เช่น การใช้ argument tuple หรือ argument dict:

class Time:
  def __init__ (self, *args):
    if len (args) == 1 and type (args[0]) == str:
      h, m, s = args[0].split(":")
      self.h, self.m, self.s = int(h), int(m), int(s)
    else:
      self.h, self.m, self.s = 0, 0, 0
      if len (args) >= 1:
        self.h = int(args[0])
      if len (args) >= 2:
        self.m = int(args[1])
      if len (args) >= 3:
        self.s = int(args[2])

t1 = Time()
t2 = Time(8)
t3 = Time(8, 20)
t4 = Time(8, 20, 45)
t5 = Time("8:20:50")

หรือใช้ factory method ซะเลย:

class Time:
  @classmethod
  def from_str (cls, s):
    h, m, s = s.split(":")
    return cls (int(h), int(m), int(s))

  @classmethod
  def from_hms (cls, h=0, m=0, s=0):
    return cls (h, m, s)

  def __init__ (self, h, m, s):
    self.h, self.m, self.s = h, m, s

t1 = Time.from_hms()
t2 = Time.from_hms(8)
t3 = Time.from_hms(8, 20)
t4 = Time.from_hms(8, 20, 45)
t5 = Time.from_str("8:20:50")

(ดัดแปลงจาก กระทู้ Stack Overflow)

การโอเวอร์โหลด operator

สมมุติว่าเรามีคลาส Date (วันที่) ซึ่งต้องการโอเวอร์โหลดเครื่องหมายลบดังนี้:

d1 = Date("2016-04-26")
d2 = Date("2016-05-03")
assert d2 - d1 == 7
assert d2 - 7 == d1

กล่าวคือ:

  • ถ้าตัวลบเป็นชนิด Date ให้หาจำนวนวันระหว่างวันที่ทั้งสอง
  • ถ้าตัวลบเป็นชนิด int ให้หาวันที่ถอยหลังเป็นจำนวนวันที่ลบ

กรณีนี้ดูจะไม่มีทางอื่น นอกจากตรวจสอบชนิดของตัวลบเอา:

class Date:
  # ...

  def __sub__ (self, other):
    if type (other) == int:
      # ... subtract days from self ...
    elif type (other) == Date:
      # ... subtract two Dates ...
    else:
      raise TypeError ("Invalid operand type")

ซึ่งออกจะดูอัปลักษณ์ถ้าเป็นโค้ด C++ แต่นี่คือไพธอนมาตรฐาน

อย่างไรก็ดี เท่าที่ลองค้นดู ดูเหมือนจะมีแพกเกจ overload และ multimethod เพื่อการนี้ และดูจะมี PEP 3124 เพื่อเพิ่มการรองรับการโอเวอร์โหลดฟังก์ชันอย่างเป็นทางการ แต่ยังอยู่ในสถานะ Deferred อยู่

Polymorphism?

หนึ่งในเครื่องมือที่ใช้กันมากของ OOP ก้คือ polymorphism ซึ่งในภาษา C++ จะใช้ virtual function ประกอบกันกับ class hierarchy ซึ่ง implementation ภายในคือ pointer to function ใน v-table ของคลาส

แต่พอมาเขียนไพธอน คุณจะหา keyword หรือ naming convention ที่เทียบเคียงกับ virtual function ไม่ได้เลย ทั้งนี้เพราะไพธอนเป็น polymorphic โดยธรรมชาติอยู่แล้ว!

เวลาที่เขียนฟังก์ชันแบบนี้ใน C++:

inline int max (int x, int y) { return (x > y) ? x : y; }
inline int max (double x, double y) { return (x > y) ? x : y; }

หรือจะใช้ generic programming ด้วย template ซึ่ง implementation ภายในเทียบเท่ากัน แต่ใช้กับชนิดใดก็ได้ที่รองรับ operator > :

template <class T>
inline int max (T x, T y) { return (x > y) ? x : y; }

แต่ในไพธอนคุณเขียนแค่นี้ก็ทำงานได้กับทุกชนิดแล้ว:

def max (x, y):
  return x if x > y else y

เพราะ dynamic typing นั่นเอง ทำให้สามารถส่งออบเจกต์ชนิดไหนก็ได้ แล้วไปว่ากันที่ run-time ถ้าชนิดนั้น ๆ รองรับ operation ที่เรียกใช้ ก็เป็นอันใช้ได้ ตามแนวคิดที่เรียกว่า duck-typing ซึ่งกล่าวว่า If it looks like a duck and quacks like a duck, it must be a duck.

คุณจึงสามารถใช้ polymorphism ได้ ไม่ว่าจะเขียนโค้ดแบบนี้:

class Animal:
  def cry (self):
    pass

class Cat (Animal):
  def cry (self):
    print ("Meow!")

class Duck (Animal):
  def cry (self):
    print ("Quack!")

farm = []
farm.append (Cat())
farm.append (Duck())

for i in farm:
  i.cry()

หรือแบบนี้:

class Cat:
  def cry (self):
    print ("Meow!")

class Duck:
  def cry (self):
    print ("Quack!")

farm = []
farm.append (Cat())
farm.append (Duck())

for i in farm:
  i.cry()

คงพอเห็นภาพนะครับ

blog นี้เขียนถึงสิ่งที่สะดุด ก็ชักจะยาวแล้ว เดี๋ยว blog หน้าค่อยเขียนถึงสิ่งที่ผมชอบในไพธอนต่อนะครับ

หมายเหตุ: ด้วยความที่ผมยังเตาะแตะกับไพธอนอยู่ ที่เขียนไปอาจมีข้อผิดพลาดจากความไม่เข้าใจ ก็ยินดีรับข้อชี้แนะจากผู้รู้ครับ

ป้ายกำกับ: , ,

04 กุมภาพันธ์ 2559

Thai Font Metrics

ประเด็นเล็ก ๆ ประเด็นหนึ่งที่ถูกอภิปรายกันในหลายโอกาสในหมู่คนทำฟอนต์ไทย หรือกระทั่งในหมู่ผู้ใช้ที่ต้องเตรียมเอกสารให้เข้ากับข้อกำหนด คือเรื่องขนาดของฟอนต์ไทยที่จะเล็กกว่าฟอนต์ตะวันตกที่ point size เดียวกัน เช่น บทความภาษาอังกฤษอาจกำหนดขนาดฟอนต์เป็น 10 หรือ 11 point แต่ถ้าใช้ฟอนต์ไทยขนาดเดียวกันจะเล็กจนอ่านไม่ออก ต้องปรับขนาดเพิ่มเป็น 14 หรือ 16 point ถึงจะเทียบเคียงกันได้ เรื่องนี้คุณเนยสดได้ เขียนอธิบายไว้แล้วเป็นอย่างดี

ฟอนต์จาก TLWG

สำหรับฟอนต์ชุดต่าง ๆ ที่ TLWG ผมได้ปรับขนาดตัวอักษรให้ใหญ่ขึ้น เพื่อให้เข้ากันกับฟอนต์ตะวันตกทั้งหมด ไม่ว่าจะเป็น Fonts-TLWG, Fonts-SIPA-Arundina หรือ ThaiFonts-Siampradesh ทำให้เกิดความไม่เข้ากันกับฟอนต์ไทยอื่น ๆ ที่มีอยู่ในตลาด

สำหรับที่มาที่ไป ผมตัดสินใจใช้ font metrics ที่เข้ากันกับฟอนต์ตะวันตกหลังจากที่ได้พูดคุยกับผู้ใช้และนักพัฒนาใน thread หนึ่งใน gtk-i18n list เมื่อปี 2547 โดยในขณะนั้น ฟอนต์ต่าง ๆ ที่พัฒนา ปรับปรุง และเผยแพร่โดย TLWG ยังใช้ขนาดเหมือนฟอนต์ไทยในตลาด และมีฝรั่งที่พยายามเรียนภาษาไทยบ่นเข้ามาว่าฟอนต์ไทยตัวเล็กมาก อ่านไม่ออก มีฟอนต์ที่ตัวใหญ่กว่านี้แนะนำไหม ผมพยายามอธิบายเหตุผลที่ฟอนต์ไทยต้องตัวเล็ก ว่าเราต้องเผื่อเนื้อที่ให้กับสระบน-ล่างและวรรณยุกต์ ก็ปรากฏว่านักพัฒนา Pango (Owen Taylor) ได้ แนะนำ ว่าสามารถใช้ขนาดตัวอักษรที่เท่ากับฟอนต์ตะวันตกได้ โดยเพียงแต่ขยายระยะระหว่างบรรทัดให้สูงขึ้น และได้รับการ สำทับ จาก Javier Sola นักพัฒนา Khmer OS ว่าฟอนต์ภาษาเขมรก็ใช้วิธีนี้ และได้ผลดี

ในขณะนั้น เรามีฟอนต์ Loma จาก NECTEC ที่เริ่มบุกเบิกทำฟอนต์ UI โดยใช้ metrics ที่สอดคล้องกับฟอนต์ตะวันตกเป็นตัวอย่างอยู่แล้ว หลังจากที่ในวินโดวส์มีฟอนต์ Tahoma ที่ทำงานในลักษณะนี้มาแล้วระยะหนึ่ง เมื่อนำมาประกอบกับข้อมูลที่ได้จากชุมชน GTK+ ดังกล่าว ผมจึงตัดสินใจปรับขนาดฟอนต์ไทยทั้งหมดในแหล่งของ TLWG ตามฟอนต์ตะวันตกตั้งแต่นั้นมา

Font Metrics แบบไทย

ย้อนกลับไปที่ที่มาของ font metrics แบบไทยที่มีการย่อส่วนลงมา เหตุผลเป็นที่เข้าใจได้ไม่ยาก ว่ามาจากการเผื่อเนื้อที่ให้กับสระบน-ล่างและวรรณยุกต์ ทำให้ต้องย่อขนาดของพยัญชนะลง และเพื่อให้เข้ากันกับตัวโรมัน ก็จำเป็นต้องย่อขนาดของตัวโรมันลงตามด้วย ทำให้ตัวโรมันของฟอนต์ไทยมีขนาดเหลือเพียงประมาณ 70% ของฟอนต์ตะวันตกที่มี point size เท่ากัน

เรื่องนี้มีตัวอย่างอย่างละเอียดในหนังสือ แบบตัวพิมพ์ไทย ที่จัดพิมพ์โดยโครงการฟอนต์แห่งชาติของ NECTEC เมื่อ พ.ศ. 2543

Thai font metrics recommendation

ปัญหา

การใช้ font metrics แบบไทยดูจะทำงานได้ดี และเราก็ใช้งานแบบนี้กันมานาน ตั้งแต่ยุค Windows 3.1 ที่ยังใช้รหัส สมอ. และมี Windows Thai Edition ใช้งานกันอยู่ มาจนถึงยุคเปลี่ยนผ่านสู่ I18N และ Unicode ใน Windows 95, Windows XP แล้วเราก็เริ่มเจออะไรทำนองนี้ในเว็บไซต์:

Mixed Thai/English text

ภาพนี้ผมจำลองขึ้นใหม่ เนื่องจากไม่มีระบบรุ่นเก่าให้จับภาพแล้ว แต่คงจะพอจำความรู้สึกนี้ได้ ที่เวลาอ่านเว็บไซต์ต่าง ๆ แล้ว เจอข้อความที่ภาษาไทยเล็กเท่ามด ภาษาอังกฤษใหญ่เท่าหม้อข้าว เพราะ text rendering engine แบบ multilingual ยุคแรก ๆ พยายามแสดงข้อความโดยแยกฟอนต์ตามภาษาเขียน แต่มันไม่แยกแยะว่าภาษาไทยต้องขยาย หรือภาษาอังกฤษต้องย่อ ส่วนข้อความภาษาเขมรนั้น ผมจำลองใส่เข้าไปด้วยเพื่อเปรียบเทียบให้ดูว่าเขา implement แบบไหน

rendering engine ยุคหลัง ๆ เริ่มฉลาดขึ้น โดยพยายามเลือกฟอนต์ตามภาษาหลักหรือตามโลแคล ถ้าฟอนต์นั้นมีอักษรโรมันให้ก็นำมาใช้เลย ถ้าไม่มีจึงจะไปหาจากฟอนต์อื่น ช่วยบรรเทาปัญหาข้อความผสมภาษาไทย-อังกฤษลงได้

แล้วโลกก็หมุนต่อไป สังคมไทยเริ่มเข้าสู่ยุคที่มีภาษาที่สามที่สี่ เด็ก ๆ เริ่มเรียนภาษาจีน ญี่ปุ่น เกาหลี ฯลฯ กันมากขึ้น ประชาคมเศรษฐกิจอาเซียนเริ่มรวมตัวกัน เริ่มมีข้อความภาษาลาว เขมร พม่า เข้ามามากขึ้น การใช้งานแบบ multilingual เริ่มเข้มข้นขึ้นกว่าแต่ก่อนที่เคยมีแค่ไทย-อังกฤษ

คำถามคือ เราจะจัดการระบบ multilingual นี้อย่างไร? เดิมมีแค่สองภาษา ฟอนต์อังกฤษมันโตเกินไป เราก็เพิ่มอักษรอังกฤษย่อส่วนลงในฟอนต์ไทยเสียก็สิ้นเรื่อง แต่เมื่อมีภาษาจีน ญี่ปุ่น เกาหลี ลาว เขมร พม่า เข้ามาร่วมด้วย เราจะเพิ่มอักษรของภาษาเหล่านั้นลงในฟอนต์ไทยหมดไหม? เอาแค่อักษรจีนอย่างต่ำ 5,000 ตัวก็อ่วมอรไทแล้ว ไหนจะอักษรเขมรที่มีอักษรตัวเชิง ทำให้ต้องเผื่อเนื้อที่บน-ล่างกว้างกว่าอักษรไทยเสียอีก เราจะได้ฟอนต์ตัวเล็กลงไปอีก และถ้าสักวันเราจะเพิ่ม อักษรทิเบต (ไม่แน่นะครับ ก็เราชอบประเทศภูฏานกันมากไม่ใช่หรือ) ที่ซ้อนกันสนุกสนานกว่าอักษรเขมรเสียอีก เราจะยิ่งได้ฟอนต์ที่เล็กกระจิ๋วหลิวเลยทีเดียว

Tibetan sample text

เริ่มเห็นภาพกันไหมครับ ว่าการเผื่อช่องว่างสำหรับการซ้อนอักขระมันไม่ scale ในระบบ multilingual

หรือหากผลักภาระไปให้ rendering engine ที่จะต้องจดจำว่าภาษาแต่ละภาษาต้องย่อ-ขยายด้วยอัตราเท่าไรแล้วปรับขนาดฟอนต์เอา มันก็เป็น workaround ที่ไปเพิ่มความยุ่งยากให้กับการออกแบบฟอนต์คู่สองภาษาอื่น ๆ เช่น การสร้างฟอนต์ที่มีอักษรไทย-ลาว, ไทย-เขมร, ไทย-พม่า, ไทย-ยาวี เพื่อใช้ในบริบทของกลุ่มผู้ใช้ทวิภาษา จะต้องออกแบบฟอนต์ให้อักษรแต่ละภาษามีขนาดไม่เท่ากันในฟอนต์เดียวกัน และถ้าเกิดว่า rendering engine ของแต่ละระบบใช้อัตราย่อ-ขยายของแต่ละภาษาต่างกันอีกล่ะ? คงจะพอจินตนาการถึงความยุ่งยากกันได้ และโชคดีที่ไม่มี rendering engine ไหนคิดทำอะไรทำนองนี้

ถ้าเช่นนั้น แบบไหนล่ะถึงจะ scale?

ทางออก

ระบบที่ดูสมเหตุสมผลกว่าก็คือ ทุกภาษาควรอิงบรรทัดฐานเดียวกัน กล่าวคือ

กำหนดให้ point size คือความสูงของตัวอักษร ไม่ใช่ความสูงของบรรทัด

เมื่อกำหนดอย่างนี้ แล้วไปขยายขนาดตัวอักษรไทยทั้งหมดให้สูงเท่า point size ซึ่งจะทำให้วรรณยุกต์เขยิบขึ้นสูงจนตกขอบด้านบนของ em-box นักพัฒนาฟอนต์ก็อาจเกิดประเด็นคำถามต่อไปนี้:

  • จะกำหนดความสูงของบรรทัดให้สูงกว่า em-box ได้อย่างไร?
  • การที่ glyph ของวรรณยุกต์ตกขอบ em-box ซึ่งเป็น bounding box ออกไป จะไม่ผิดหลักการหรือ?
  • font metrics แบบใหม่ จะขัดกันกับแบบเดิมที่ใช้ในเอกสารต่าง ๆ ไหม?

ก็จะตอบทีละประเด็นนะครับ

การกำหนดความสูงของบรรทัด

โดยปกติที่ผ่านมา ฟอนต์ไทยเคร่งครัดกับเรื่องความสูงของบรรทัดที่จะไม่ให้เกิน em-box ทำให้เราต้องย่อขนาดตัวอักษรทั้งหมดให้เล็กลงเพื่อให้สามารถบรรจุสระบน-ล่างและวรรณยุกต์ลงไปใน em-box ได้

แต่ในการคำนวณระยะระหว่างบรรทัดของโปรแกรมต่าง ๆ ที่ใช้ฟอนต์ TrueType หรือ OpenType จะไม่ได้ใช้ ascender/descender จาก em-box นี้โดยตรง แต่จะใช้ค่า sTypoAscender, sTypoDescender และ sTypoLineGap จาก ตาราง OS/2 & Windows Metrics ในตัวฟอนต์ ซึ่งเราสามารถกำหนดค่า sTypoAscender และ sTypoDescender ให้เลย em-box ออกไปได้ และอาจกำหนดค่า usWinAscent และ usWinDescent ด้วย เพื่อไม่ให้บิตแม็ปของตัวอักษรที่วาดถูกขลิบออก

อย่างไรก็ดี ในทางปฏิบัติแล้ว ค่า usWinAscent และ usWinDescent กลับเป็นค่าที่มีผลต่อการคำนวณระยะระหว่างบรรทัดมากกว่า (app ต่าง ๆ ไม่ได้ทำตาม spec ของไมโครซอฟท์นัก แม้ใน spec ของไมโครซอฟท์จะเขียนไว้ว่า This is strongly discouraged. ก็ตาม

โดยสรุปก็คือ ควรกำหนดค่าทั้ง sTypoAscender, sTypoDescender, usWinAscent, usWinDescent ทั้งหมดไว้ก่อน

หากใช้ Fontforge ก็กำหนดได้ในแท็บ OS/2 Metrics (Element > Font Info > OS/2 > Metrics)

Fontforge OS/2 Metrics dialog

ตำแหน่งของวรรณยุกต์

ในยุคก่อน เรามีการจัดตำแหน่งสระบน-ล่างและวรรณยุกต์เพื่อหลบหางพยัญชนะโดยใช้วิธีสร้าง glyph ชุดพิเศษที่มีการเลื่อนตำแหน่งรอไว้ แล้ว rendering engine จะเลือกใช้ glyph ชุดพิเศษเหล่านั้นตามความเหมาะสม เป็นเทคนิคที่เขาเรียกกันว่า positioning by substitution

glyph ชุดพิเศษต่าง ๆ เหล่านี้ จะเลื่อนระยะไว้เรียบร้อยเพื่อให้นำไปวางซ้อนได้พอดีโดยไม่มีการเลื่อนที่อีก ดังนั้น หากจะใช้เทคนิคนี้กับฟอนต์ที่ขยายขนาดขึ้น glyph ของวรรณยุกต์ที่เลื่อนระยะรอไว้ก็จะต้องตกขอบ em-box อย่างเลี่ยงไม่ได้ (แต่ยังอยู่ภายในความสูงของบรรทัด)

แต่นั่นเป็นข้อจำกัดของระบบเก่าก่อนที่จะมี OpenType

ด้วยเทคโนโลยี OpenType เรามีเครื่องมือจัดตำแหน่งการวางซ้อนอักขระ โดยใช้ข้อมูล GPOS ซึ่งอาศัยการกำหนด anchor สำหรับวาง glyph ซ้อนกันเหมือนการต่อชิ้นส่วนเลโก้ โดย glyph ที่เป็นฐานจะมี base anchor เป็นเหมือนเบ้าเสียบรอไว้ ส่วน glyph ที่จะมาวางซ้อนก็จะมี mark anchor เป็นเหมือนเดือยสำหรับเสียบเข้ากับเบ้า

ในการวาง glyph ที่มาซ้อนนั้น ตำแหน่ง glyph ตัวสวมจะถูกเลื่อนที่ด้วยเวกเตอร์ระหว่าง anchor เสียบกับ anchor เบ้า เพื่อให้ anchor สวมกันได้พอดี ดังนั้น ตำแหน่งเดิมของ glyph จะอยู่ที่ไหนก็ไม่สำคัญ เพราะยังไงก็สามารถคำนวณเวกเตอร์ดังกล่าวได้อยู่แล้ว ทำให้เราสามารถวาง glyph ของวรรณยุกต์ไว้ภายใน em-box ก็ได้ถ้าต้องการ นักพัฒนาฟอนต์ที่เคร่งครัดกับ em-box จึงวางใจได้

Thai composition example

สำหรับผู้ที่สนใจ ไมโครซอฟท์มีเอกสารแนะนำ การสร้างฟอนต์ OpenType สำหรับอักษรไทย โดยเฉพาะ

ความเข้ากันได้กับฟอนต์เดิม

ประเด็นนี้ ตอบได้สั้น ๆ ว่า ไม่เข้ากันแน่นอน หากจะเปลี่ยนมาใช้ metrics ใหม่ จะต้องมีการจัดการกับความเปลี่ยนแปลง และคงไม่ใช่เรื่องที่จะทำได้ชั่วข้ามคืน ผมเองก็ไม่เคยคิดว่ามันจะเกิดในอนาคตอันใกล้ จนกระทั่งเริ่มสังเกตเห็นแนวโน้มความเปลี่ยนแปลงบางอย่าง กล่าวคือ

  • ฟอนต์นานาภาษา ที่มีอักษรของภาษาต่าง ๆ ทั่วโลกเริ่มมีมากขึ้น หลังจากเทคโนโลยี multilingual เริ่มอยู่ตัวและใช้กันเป็นปกติ เช่น
    • Freefont จาก GNU Project (มีอักษรไทย)
    • Noto Fonts จาก Google (มีอักษรไทย)
    • DejaVu fork จาก Bitstream Vera (ยังไม่มีอักษรไทย)
    เฉพาะฟอนต์ที่มีอักษรไทย แน่นอนว่าใช้ font metrics ที่ point size = ขนาดตัวอักษร และใช้ GPOS ในการเลื่อนวรรณยุกต์ขึ้นสูงจาก em-box หรือถ้ายังไม่มีอักษรไทย ดูอักษรภาษาอื่น ๆ ที่มีแล้วต่างก็ใช้แนวทางนี้
  • web font ไทย
    • คัดสรร ดีมาก (Cadson Demak) เท่าที่ตรวจสอบเฉพาะ 3 ฟอนต์ใน Google Fonts พบว่าใช้ font metrics ที่ point size = ขนาดตัวอักษร และใช้ GPOS ในการเลื่อนวรรณยุกต์ขึ้นสูงจาก em-box

หากแนวโน้มการสร้างฟอนต์นานาภาษาเพื่อใช้ในระบบต่าง ๆ มีมากขึ้น (ผมยังไม่ได้ตรวจสอบฟอนต์ที่ใช้ในสมาร์ทโฟนต่าง ๆ) และการใช้ฟอนต์ในเว็บไซต์ต่าง ๆ ก็มีมากขึ้น จึงน่าสนใจว่าแนวโน้มเหล่านี้จะมีผลเปลี่ยนแปลงการใช้ฟอนต์ในเดสก์ท็อปปัจจุบันมากน้อยแค่ไหน คงต้องรอดูกันต่อไป โดยเฉพาะบนวินโดวส์ที่มีฟอนต์ Tahoma ให้ใช้กันเป็นตัวอย่างมานานแล้ว

และสุดท้ายก็อยู่ที่ผู้พัฒนาฟอนต์ทั้งหลายนั่นแหละครับ ว่าจะขยับไปสู่แนวทางนี้กันมากน้อยแค่ไหน

ป้ายกำกับ: ,

hacker emblem