Theppitak's blog

My personal blog.

08 กุมภาพันธ์ 2560

Dell Inspiron 5468 with Debian Power

TL;DR แล็ปท็อปใหม่ของผม Dell Inspiron 5468 ใช้ CPU Intel Core i5-7200U, RAM 4 GB และ SSD 256 GB ติดตั้ง Ubuntu มาพร้อม นำมาติดตั้ง Debian Sid แบบ dual boot ใช้การได้ดี ยกเว้นมีปัญหา Wi-Fi, ไม่ชินกับปุ่ม PgUp/PgDn/Home/End, และเสียงไม่ออกหูฟังโดยอัตโนมัติเมื่อเสียบ

หลังจากที่ใช้แล็ปท็อปเก่ามาได้ร่วม 6 ปี จนเริ่มออกอาการหลายอย่าง เช่น ร้อนจนดับบ่อย เล่นวิดีโอได้ไม่เกิน 5 นาที เรื่อง video call ไม่ต้องพูดถึงเลย และล่าสุดคือปุ่ม power เริ่มรวน เปิดติดบ้างไม่ติดบ้าง ต้องแก้ปัญหาด้วยวิธีแปลก ๆ เช่น ลองถอดแบตแล้วเสียบปลั๊กไปเรื่อย ๆ จนกว่าจะกดติด ก็คิดว่าคงได้เวลาซื้อแล็ปท็อปใหม่เสียที (ด้วยแรงสนับสนุนของคุณภรรยา)

Spec ที่ต้องการ

  • CPU เพื่องานคอมไพล์โปรแกรมและการเปิดหน้าต่างหลายบานทำงานพร้อมกันเยอะหน่อย ผมจึงคิดว่าผมต้องการ Core i5 หรือ i7 เพื่อประโยชน์จาก Turbo Boost (ลิงก์ที่น่าสนใจ: Best Intel Processor: Core i3, i5, i7 explained)
  • การ์ดจอ ผมไม่ต้องการการ์ดจอที่แรงนัก ได้ Intel on-board ได้ยิ่งดี เพราะไม่ต้องไปวุ่นวายหาไดรเวอร์ที่ไม่โอเพนซอร์ส แต่พบว่าเครื่องรุ่นใหม่ ๆ โดยเฉพาะ CPU Core i5, i7 มักจะมาพร้อมการ์ดจอแยก (คงเพื่อให้ประสิทธิภาพกราฟิกส์สมน้ำสมเนื้อกับความแรงของ CPU) การเจาะจง Intel on-board จะจำกัดตัวเลือกลงมากจนหาซื้อไม่ได้เลยจากร้านใกล้บ้าน ถ้าเช่นนั้น AMD ก็เป็นทางเลือกที่น่าสนใจ เพราะทำให้ราคาถูกลง โดยประสิทธิภาพก็ไม่ได้แย่เกินไปนัก (Test 1, Test 2) และยังเป็นมิตรกับโอเพนซอร์สกว่า NVIDIA อีกด้วย
  • จอภาพ ต้องการจอ 14" เพื่อความสะดวกในการพกพา (15" เทอะทะเกินไป) แต่ไม่เล็กจนเกินไป (11", 13" เนื้อที่ทำงานน้อยเกินไป) ไม่เกี่ยงชนิดของจอ ไม่จำเป็นต้อง IPS
  • OS อาจจะดูแปลกสำหรับผู้ใช้ทั่วไป แต่ผมจะให้ความสนใจกับแล็ปท็อปรุ่นที่ไม่มีไลเซนส์วินโดวส์เป็นพิเศษ เพราะผมไม่ใช้วินโดวส์อยู่แล้ว ได้ไลเซนส์มาก็เปลืองเงินเฉย ๆ ยิ่งถ้าเป็นรุ่นที่มาพร้อมลินุกซ์เลย (เช่น Ubuntu) ยิ่งน่าสนใจ เพราะเท่ากับลดความเสี่ยงที่จะใช้กับลินุกซ์ไม่ได้ลงไปเยอะ แต่รุ่นที่ลง DOS มาให้ก็สนใจเป็นอันดับรองลงไปเช่นกัน

สี่อย่างนี้ ผมใช้เป็นตัว screen เบื้องต้น แล้วค่อยไปเช็กอย่างอื่นเป็นรายตัวไป เช่น RAM, ฮาร์ดดิสก์, พอร์ตต่าง ๆ ฯลฯ

Dell Inspiron 5468

ตัดฉับมาตอนที่ผมหิ้วเครื่องออกจากร้าน มันคือ Dell Inspiron 5468 โดยมี spec ดังนี้:

  • CPU: Intel Core i5-7200U (3 MB L3 Cache, 2.50 GHz)
  • Graphic card: AMD Radeon R7 M440 (2 GB DDR3)
  • Display: 14" HD (1366x768) anti-glare
  • Memory: 4 GB DDR4
  • Hard Drive: 256 GB SATA SSD (INTEL SSDSC2KF256H6)
  • OS Bundle: Ubuntu 16.04 LTS

รายละเอียดอื่น ๆ ดูได้จาก Dell แต่ในรุ่นนี้ มี CPU ให้เลือกสองแบบ คือรุ่น Core i5 และรุ่น Core i7 ราคาต่างกันอยู่ 3,000 บาท โดยรุ่น Core i5 นั้น ได้ SSD ขนาด 256 GB ในขณะที่รุ่น Core i7 ได้ HDD ขนาด 1 TB (5400 RPM) ผมเลือกรุ่น Core i5 เพราะ SSD เลยทีเดียว

รายละเอียดอื่น ๆ

  • DVD Writer: HL-DT-ST DVD+-RW GU90N
  • Wi-Fi: Qualcomm Atheros QCA9377 802.11ac
  • Ethernet: Realtek RTL8101/2/6E PCIe Gigabit Ethernet
  • Bluetooth: Qualcomm Atheros (ID 0cf3:e009)
  • SD Card Reader: Realtek RTS5129 (ID 0bda:0129)
  • Webcam: Realtek Integrated Webcam (ID 0bda:5684)

dump ข้อมูลที่เป็นประโยชน์สำหรับผู้ที่จะซื้อมาใช้กับลินุกซ์ดังนี้ (จัดบรรทัด output เล็กน้อยเพื่อให้อ่านง่าย):

$ lscpu

Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                4
On-line CPU(s) list:   0-3
Thread(s) per core:    2
Core(s) per socket:    2
Socket(s):             1
NUMA node(s):          1
Vendor ID:             GenuineIntel
CPU family:            6
Model:                 142
Model name:            Intel(R) Core(TM) i5-7200U CPU @ 2.50GHz
Stepping:              9
CPU MHz:               797.772
CPU max MHz:           3100.0000
CPU min MHz:           400.0000
BogoMIPS:              5424.00
Virtualization:        VT-x
L1d cache:             32K
L1i cache:             32K
L2 cache:              256K
L3 cache:              3072K
NUMA node0 CPU(s):     0-3
Flags:                 fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca
  cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx
  pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl
  xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl
  vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe
  popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch
  epb intel_pt tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1
  avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt xsaveopt xsavec
  xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp

$ lspci

00:00.0 Host bridge: Intel Corporation Device 5904 (rev 02)
00:02.0 VGA compatible controller: Intel Corporation Device 5916 (rev 02)
00:14.0 USB controller: Intel Corporation Sunrise Point-LP USB 3.0 xHCI
  Controller (rev 21)
00:14.2 Signal processing controller: Intel Corporation Sunrise Point-LP
  Thermal subsystem (rev 21)
00:15.0 Signal processing controller: Intel Corporation Sunrise Point-LP Serial
  IO I2C Controller #0 (rev 21)
00:15.1 Signal processing controller: Intel Corporation Sunrise Point-LP Serial
  IO I2C Controller #1 (rev 21)
00:16.0 Communication controller: Intel Corporation Sunrise Point-LP CSME HECI
  #1 (rev 21)
00:17.0 SATA controller: Intel Corporation Sunrise Point-LP SATA Controller
  [AHCI mode] (rev 21)
00:1c.0 PCI bridge: Intel Corporation Device 9d10 (rev f1)
00:1c.4 PCI bridge: Intel Corporation Sunrise Point-LP PCI Express Root Port #5
  (rev f1)
00:1c.5 PCI bridge: Intel Corporation Sunrise Point-LP PCI Express Root Port #6
  (rev f1)
00:1f.0 ISA bridge: Intel Corporation Device 9d58 (rev 21)
00:1f.2 Memory controller: Intel Corporation Sunrise Point-LP PMC (rev 21)
00:1f.3 Audio device: Intel Corporation Device 9d71 (rev 21)
00:1f.4 SMBus: Intel Corporation Sunrise Point-LP SMBus (rev 21)
01:00.0 Display controller: Advanced Micro Devices, Inc. [AMD/ATI] Topaz XT
  [Radeon R7 M260/M265 / M340/M360 / M440/M445] (rev 83)
02:00.0 Network controller: Qualcomm Atheros QCA9377 802.11ac Wireless Network
  Adapter (rev 31)
03:00.0 Ethernet controller: Realtek Semiconductor Co., Ltd. RTL8101/2/6E PCI
  Express Fast/Gigabit Ethernet controller (rev 07)

$ lsusb

Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 007: ID 0cf3:e009 Atheros Communications, Inc. 
Bus 001 Device 005: ID 0bda:0129 Realtek Semiconductor Corp. RTS5129 Card
  Reader Controller
Bus 001 Device 003: ID 0bda:5684 Realtek Semiconductor Corp. 
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

$ lsscsi

[0:0:0:0]    disk    ATA      INTEL SSDSC2KF25 D07N  /dev/sda 
[1:0:0:0]    cd/dvd  HL-DT-ST DVD+-RW GU90N    A1C1  /dev/sr0 

Ubuntu hardware certification ระบุว่า Ubuntu ที่ติดตั้งมาจากโรงงานมีปัญหา hibernate ไม่ได้ และ Bluetooth 4.0 HID ไม่ทำงาน แต่ก็รายงาน wireless adaptor เป็น Intel Wireless 3165 ในขณะที่ในเครื่องจริง ๆ เป็น Qualcomm Atheros QCA9377

เตรียมพาร์ทิชัน

เครื่องมาพร้อมกับ Ubuntu 16.04 LTS ติดตั้งไว้ โดยแบ่งพาร์ทิชันดังนี้:

$ fdisk -l /dev/sda

Disk /dev/sda: 238.5 GiB, 256060514304 bytes, 500118192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt

Device         Start       End   Sectors   Size Type
/dev/sda1       2048   1026047   1024000   500M EFI System
/dev/sda2    1026048   7317503   6291456     3G Microsoft basic data
/dev/sda3    7317504 483756031 476438528 227.2G Linux filesystem
/dev/sda4  483756032 500117503  16361472   7.8G Linux swap

สองพาร์ทิชันแรก (sda1, sda2) ใช้สำหรับ UEFI boot พาร์ทิชันที่สาม (sda3) เป็น Ubuntu 16.04 LTS โดยเมานท์กับ / เพียงจุดเดียว และที่ท้ายดิสก์เป็นพาร์ทิชัน swap 7.8 GB (ประมาณสองเท่าของ RAM 4 GB)

หากจะติดตั้ง Debian โดยลบ sda3 ทิ้งไปเลยก็ย่อมได้ แต่เห็นว่า Ubuntu image ที่ติดตั้งมาจากโรงงานอาจพอมีประโยชน์สำหรับอ้างอิงอาการต่าง ๆ ที่อาจพบในอนาคต ระหว่าง config จากโรงงานกับที่เราติดตั้งเอง และอาจใช้ลอกการบ้านในกรณีที่มีปัญหากับฮาร์ดแวร์ได้ จึงตัดสินใจว่าจะหดพาร์ทิชัน Ubuntu แล้วสร้างพาร์ทิชันใหม่สำหรับ Debian และ /home

วิธีการที่ใช้คือ:

  1. ดาวน์โหลด Debian Installer จากหน้าโครงการ Debian-installer โดยผมเลือก Current daily snapshot สำหรับ amd64
  2. เขียน installer image โดยเสียบ USB flash drive เปล่า แล้วตรวจหา device file ของมันด้วยคำสั่ง dmesg แล้วไล่หาดูอุปกรณ์ล่าสุดใน log ซึ่งในกรณีของผมมันคือ /dev/sdb จากนั้นก็สั่งเขียน:
    # dd if=debian-testing-amd64-netinst.iso of=/dev/sdb
    
  3. เตรียม firmware โดยดาวน์โหลด firmware tarball จาก Debian cdimage มารอไว้เพื่อเขียนลง flash drive โดยจากขั้นตอนที่แล้ว ISO image ที่เขียนลงไปจะทำให้ใน flash drive จะมี 2 partition เกิดขึ้น พาร์ทิชันแรกใช้สำหรับบูต Debian installer และเราจะเขียน firmware ลงในพาร์ทิชันที่สอง โดยอาจจะแตก tarball ทั้งหมดลงไปเลย:
    # mount /dev/sdb2 /mnt
    # tar xzf firmware.tar.gz -C /mnt
    # umount /mnt
    
    หรือเลือกเฉพาะที่ต้องใช้สำหรับเครื่องนี้ คือ firmware-atheros และ firmware-realtek:
    $ tar xzf firmware.tar.gz
    # su
    Password:
    # mount /dev/sdb2 /mnt
    # cp firmware-atheros*.deb firmware-realtek*.deb /mnt
    # umount /mnt
    
  4. บูต Installer โดย disable secure boot เสียก่อน (ผมไม่แน่ใจว่า Debian installer สามารถบูตใน secure mode ได้หรือเปล่า แต่เนื่องจากผมมีเวลาไม่มาก จึงรวบรัดตัดตอน และเครื่อง Dell นี้ก็สามารถปิด secure boot ได้ โดยกด F12 ขณะเปิดเครื่องแล้วเซ็ต boot mode เป็น legacy mode คือ UEFI with secure boot OFF จากนั้นก็เซ็ต boot order ให้บูตจาก USB drive ก่อน internal HDD) เมื่อบูตเข้า Debian installer แล้ว ก็ตอบคำถามไปเรื่อย ๆ จนเสร็จขั้น load installer components แล้วหยุดอยู่แค่นั้น
  5. resize พาร์ทิชัน Ubuntu โดยกด Alt-F2 เพื่อไปที่ console ที่ 2 แล้วกด Enter เพื่อเข้า Busybox shell แล้วเริ่ม resize partition sda3 ตามขั้นตอนที่แนะนำใน หน้านี้ คือ:
    1. check file system
      # fsck /dev/sda3
      
    2. ปิด journal เพื่อเปลี่ยนพาร์ทิชัน ext4 ให้เป็น ext2
      # tune2fs -o ^has_journal /dev/sda3
      
    3. check file system อีกครั้งเพื่อจัดระเบียบบล็อคต่าง ๆ
      # e2fsck -f /dev/sda3
      
    4. resize file system ใน sda3
      # resize2fs /dev/sda3 15G
      
    5. resize ตัวพาร์ทิชัน sda3 ด้วย fdisk
      # fdisk /dev/sda
      
      โดยขั้นตอนที่ต้องทำคือ:
      1. ลบพาร์ทิชัน sda3 ด้วยคำสั่ง d
      2. เพิ่มพาร์ทิชัน sda3 กลับคืนด้วยขนาดใหม่ ด้วยคำสั่ง n
      3. เขียนตารางพาร์ทิชันลงดิสก์ ด้วยคำสั่ง w
      4. ออกจาก fdisk ด้วยคำสั่ง q

จากนี้ ระบบก็พร้อมแล้วสำหรับติดตั้ง Debian GNU/Linux สามารถกด Alt-F5 เพื่อกลับไป console ติดตั้งเพื่อทำงานต่อได้ หรือจะรีบูตเข้า installer อีกรอบเพื่อความแน่ใจก็ตามแต่

Debian GNU/Linux

เพื่อไม่ให้บล็อกนี้ยาวเกินไป ผมขอข้ามรายละเอียดการติดตั้ง Debian เอาเป็นว่า ผมเลือกติดตั้ง Xfce desktop เครื่องนี้จึงเป็นเครื่องที่ปลอด GNOME ยิ่งกว่าเครื่องก่อนที่อาจจะยังมีแพกเกจของ GNOME ตกค้างอยู่บ้างระหว่างย้ายมา Xfce

การใช้งานนับว่ารวดเร็วปรู๊ดปร๊าดกว่าเครื่องก่อนอย่างมโหฬาร SSD ทำให้อ่าน-เขียนดิสก์ได้เร็วแบบลื่นหัวแตก ความรู้สึกเหมือนสมัยใช้ RAM disk บน DOS ยังไงยังงั้น ใช้ aptitude upgrade/install/remove โปรแกรมได้สนุกสนาน ไม่ต้องรอนาน โหลด Firefox พร้อมประวัติและ bookmark ต่าง ๆ ที่ import มาจากเครื่องเก่าเสร็จภายใน 3 วินาที บ้าไปแล้ว! และเครื่องก็แทบไม่ร้อนเลยแม้จะเล่นวิดีโอยาว ๆ ทำให้พัดลมระบายความร้อนแทบไม่ได้ทำงานเลย นาน ๆ จะได้ยินสักครั้ง เป็นการใช้คอมพิวเตอร์ที่เงียบสนิทจนน่าตกใจ เมื่อเทียบกับเครื่องเก่า

ปัญหาที่พบ

ก่อนที่จะลงรายละเอียดที่เป็นความชอบส่วนตัว ผมขอบันทึกปัญหาที่พบเสียก่อน:

  • Wi-Fi หลุดบ่อยเมื่อใช้กับ access point ที่มีการเปลี่ยน bandwidth เป็นระยะ อ่าน syslog แล้ว พบว่ามีข้อความ firmware crash ซึ่งตรงกับรายงานใน Debian #839662 และ LP #1627474 คงต้องติดตามที่ upstream ว่ามีการแก้ไขอะไรอย่างไรบ้าง
  • แป้นพิมพ์วางปุ่ม PgUp/PgDn/Home/End ไว้ในปุ่มเดียวกับปุ่มลูกศร โดยให้กดปุ่ม Fn เพื่อเลือก ซึ่งการกดปุ่มประกอบเพิ่มอีกปุ่มทำให้ไม่สะดวกอย่างมาก
  • เสียงไม่ออกหูฟังโดยอัตโนมัติเมื่อเสียบ เสียงยังคงออกลำโพงของเครื่องตามปกติ ต้องไปเลือกอุปกรณ์เอาต์พุตใน audio mixer แบบ manual เอา (อาการใน Ubuntu หนักกว่า คือนอกจากเสียงจะไม่ออกหูฟังแล้ว ใน sound config ยังไม่มีรายการของหูฟังให้เลือกเลยด้วย)

ส่วนอื่น ๆ เช่น webcam, bluetooth ยังไม่ได้ทดสอบครับ ไว้ถ้ามีโอกาสค่อยว่ากันอีกที

การปรับแต่งซอฟต์แวร์

หัวข้อนี้เป็นการปรับแต่งซอฟต์แวร์สำหรับให้ตัวผมเองในอนาคตอ่าน ในกรณีที่ต้องเซ็ตเครื่องทำงานเครื่องใหม่

นอกจาก Xfce ปกติแล้ว ผมติดตั้งแพกเกจต่อไปนี้ของ GNOME เพิ่มเติม:

  • gucharmap เพื่อใช้ดู/ป้อนอักขระยูนิโค้ด
  • evince สำหรับเปิดเอกสาร PDF แม้ผมจะใช้ MuPDF เป็นหลัก แต่บางครั้งก็ต้องมี Evince ไว้สำรองเผื่อเหลือเผื่อขาด
  • eog แม้จะซ้ำซ้อนกับ ristretto ของ Xfce เพราะ ristretto ยังไม่สามารถสั่งหมุนภาพ 90 องศาได้ ซึ่งจำเป็นเวลานำเข้ารูปจากกล้องดิจิทัล

รายการปรับแต่งซอฟต์แวร์

  • Console:
    • ตั้งค่าใน /etc/default/keyboard:
      XKBLAYOUT="us,th"
      XKBVARIANT=",tis"
      XKBOPTIONS="grp:alt_shift_toggle,lv3:ralt_switch,terminate:ctrl_alt_bksp,\
      grp_led:scroll,ctrl:nocaps"
      
      ใช้ผังแป้นพิมพ์ มอก. 820-2538 พร้อม level 3 shift และเปลี่ยนปุ่ม CapsLock ให้เป็น Ctrl (ผมเคยอธิบายเหตุผลไว้ใน blog เก่า)
  • Xfce:
    • โปรแกรม > ตั้งค่า > ปรับละเอียดโปรแกรมจัดการหน้าต่าง:
      • สิ่งอำนวยความสะดวก > ปิดตัวเลือก เรียงหน้าต่างต่อชนโดยอัตโนมัติเมื่อย้ายไปชนขอบหน้าจอ
        ป้องกัน accidental maximization
      • พื้นที่ทำงาน > ปิดตัวเลือก ใช้การกลิ้งลูกกลิ้งเมาส์บนพื้นโต๊ะในการสลับพื้นที่ทำงาน
        ป้องกันความรำคาญจากการกลิ้งลูกกลิ้งเมาส์นอกหน้าต่างโดยบังเอิญ
    • โปรแกรม > ตั้งค่า > โปรแกรมจัดการหน้าต่าง:
      • โฟกัส > ยกเมื่อคลิก > ปิดตัวเลือก ยกหน้าต่างขึ้นเมื่อคลิกภายในหน้าต่างโปรแกรม
        เป็นความสะดวกส่วนตัว ที่บ่อยครั้งอยากดูเนื้อหาหน้าต่างหนึ่งเต็ม ๆ เพื่อใช้ประกอบการป้อนคำสั่งในอีกหน้าต่างหนึ่งที่เผยเฉพาะบริเวณที่ป้อนก็พอแล้ว ถ้าอยากดูเนื้อหาหน้าต่างที่ป้อนข้อความอยู่แบบเต็ม ๆ เมื่อไร ก็ค่อยไปคลิกที่กรอบหน้าต่างเอา
    • แอพเพล็ต สลับพื้นที่ทำงาน:
      • คุณสมบัติ > รูปปรากฏ > จำนวนแถว = 2
        เพื่อจัดวางพื้นที่ทำงานแบบ 2×2
    • โปรแกรม > ตั้งค่า > รูปลักษณ์:
      • สไตล์ = Greybird (จากแพกเกจ greybird-gtk-theme)
        เป็น theme ที่ทำให้วิดเจ็ตของ GTK+ 3 ไม่ใหญ่เทอะทะเหมือน theme มาตรฐาน
  • Vim:
    • install vim เพื่อใช้แทน vim-tiny
    • remove nano (ไม่เคยได้ใช้เลย ในเครื่องก่อน ๆ นั้น ยังประนีประนอมด้วยการเซ็ต vim ให้เป็น default editor แต่ nano ที่เหลือไว้ก็กินเนื้อที่ในระบบเฉย ๆ โดยไม่เคยถูกเรียกใช้เลย)
    • ตั้งค่าใน /etc/vim/vimrc.local:
      " Disable mouse clicks
      set mouse-=a
      
      vim 8 มาพร้อมกับการรองรับ mouse แม้จะทำงานใน text mode ซึ่งทำให้ตำแหน่งเคอร์เซอร์เปลี่ยนเมื่อมีการ copy/paste ข้ามหน้าต่างด้วยเมาส์ หรือแม้แต่การคลิกในหน้าต่างเพื่อโฟกัส และยิ่งสร้างความสับสนยิ่งขึ้นถ้ายังอยู่ใน insert mode ดังนั้น ปิดมันซะ!
      " Force syntax highlight
      syntax on
      
      vim มี default config เปลี่ยนไปมาเรื่อง syntax highlight บางรุ่นก็เปิด บางรุ่นก็ปิด ดังนั้น ตัดปัญหาด้วยการระบุสิ่งที่เราต้องการให้ชัดเสีย
    • ตั้งค่าใน ~/.vimrc:
      set modeline
      
      ด้วยเหตุผลด้านความปลอดภัย vim จึงปิดการใช้ modeline ไว้ และไม่แนะนำให้เปิดใช้เมื่อเป็น root แต่ผมใช้ในซอร์สโค้ดเพื่อความสะดวกในการควบคุมสไตล์การร่นย่อหน้า จึงเปิดใช้ในขอบเขตของ user ปกติเท่านั้น ถ้าต้องเปิด text file ที่ไม่น่าเชื่อถือ ควรใช้ less เปิดดูก่อน

สรุป

โดยรวมแล้ว ผมพอใจกับเครื่องใหม่นี้มาก มันทำให้งานผมเสร็จได้เร็วขึ้น โดยปัจจัยหลักคือ SSD ที่ทำให้ไม่ต้องรอ I/O นาน นอกจากนี้ยังพอใจกับความร้อนที่น้อย การรองรับของลินุกซ์ที่เพียงพอต่อความต้องการ มียกเว้นแค่สองเรื่องที่ค่อนข้างร้ายแรง คือ Wi-Fi ที่หลุดบ่อยเมื่อใช้กับ access point ที่มีการเปลี่ยนแบนด์วิดท์เป็นระยะ (กับ access point บางตัวไม่มีปัญหานี้) และปุ่ม Home/End/PgUp/PgDn ที่ต้องกด Fn ร่วมด้วย (external keyboard ช่วยได้ แต่ก็ต้องฝึกใช้ปุ่มแล็ปท็อปให้เคยชินเพื่อความคล่องตัวในการใช้งานนอกสถานที่ด้วย)

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

11 มกราคม 2560

Long Time No Thanks

ผมร้างจากการเขียน blog ขอบคุณผู้สนับสนุนงานพัฒนาของผมไปเกือบหนึ่งปีเต็ม (นับจาก entry ล่าสุด อีกหนึ่งเดือนก็จะครบปี) รู้สึกผิดอยู่เหมือนกัน ถึงแม้จะได้อัปเดตหน้า ขอบคุณ มาเป็นระยะ แต่เวลาไม่เอื้ออำนวยให้เขียน blog สักเท่าไร และความพยายามเพิ่มเนื้อหาสาระใน blog ด้วยประเด็นที่น่าสนใจ ก็ทำให้ผัดวันเขียน blog ขอบคุณมาเรื่อย ๆ

อย่างไรก็ดี ผมก็ขอถือโอกาสนี้ ขอบคุณย้อนหลัง 11 เดือน สำหรับผู้ที่ได้ หย่อนสตางค์ลงหมวก เพื่อสนับสนุนงานพัฒนาซอฟต์แวร์เสรีของผม ดังนี้:

  • เดือนกุมภาพันธ์ 2559
    • คุณธนาธิป ศรีวิรุฬห์ชัย
    • ผู้ไม่แสดงตน 1 ท่าน
  • เดือนมีนาคม 2559
    • คุณธนาธิป ศรีวิรุฬห์ชัย
  • เดือนเมษายน 2559
    • คุณวิทยา ไตรสารวัฒนะ
    • คุณธนาธิป ศรีวิรุฬห์ชัย
  • เดือนพฤษภาคม 2559
    • คุณธนาธิป ศรีวิรุฬห์ชัย
  • เดือนมิถุนายน 2559
    • คุณวิทยา ไตรสารวัฒนะ
  • เดือนกรกฎาคม 2559
    • คุณธนาธิป ศรีวิรุฬห์ชัย
  • เดือนสิงหาคม 2559
    • คุณวิทยา ไตรสารวัฒนะ
  • เดือนกันยายน 2559
    • ผู้ไม่ประสงค์จะออกนาม
  • เดือนธันวาคม 2559
    • คุณวิทยา ไตรสารวัฒนะ

ขอขอบคุณสำหรับการสนับสนุนที่ผ่านมาของทุกท่านครับ ขอให้ทุกท่านมีความสุขความเจริญในหน้าที่การงาน มีสุขภาพแข็งแรง คิดสิ่งใดก็ขอให้สมความปรารถนานะครับ

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

ช่วงที่ผ่านมา งานซอฟต์แวร์เสรีของผมก็ยังอยู่ใน maintenance mode เช่นเคย โดยเป็นการดูแลโครงการต่าง ๆ ของ TLWG ซึ่งขณะนี้ได้ ย้าย ไปที่ GitHub แล้ว และ release รุ่นปรับปรุงออกมาเรื่อย ๆ ตามโอกาส ดังข่าวประกาศต่าง ๆ ที่เว็บ LTN ดังกล่าว พร้อมทั้งอัปโหลด แพกเกจ เข้า Debian ตามปกติ

ส่วนงานแปล ก็ยังคงอัปเดตคำแปลของ Xfce ใน development snapshot และตรวจทานคำแปลของ GNOME ตามที่โอกาสจะอำนวย ซึ่งก็เรียกได้ว่าไม่ได้ active เท่าเมื่อก่อน แต่ก็ยังไม่เลิกทำนะครับ

ส่วนชีวิตทั่วไป ตอนนี้ผมต้องศึกษางานสาขาอื่น เพื่อให้สามารถรับงานที่ generate income ได้มากกว่างานรองรับภาษาไทย เพื่อความอยู่รอดของครอบครัวครับ เรื่องที่ไม่เคยทำก็หัดเสียให้เป็น ก็พร้อมรับคำชี้แนะจากผู้รู้ และเปิดรับงานนอกสาขาเดิม ส่วนจะทำได้มากน้อยแค่ไหนก็ต้องพิจารณาศักยภาพของตัวเองเป็นกรณีไปครับ

ป้ายกำกับ:

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

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

hacker emblem