What is fennel?

Fennel คือผักชีล้อมครับหน้าตาเป็นอย่างไรหาดูในเน็ตได้สมัยนี้แล้ว ( ผมเองก็ไม่รู้จัก 555 ) อ้าวแล้วจะพูดถึงทำไม คือ Fennel ที่ผมพูดถึงนี่ไม่ใช่พืชผักครับแต่เป็นภาษาโปรแกรมภาษาหนึ่งซึ่งเมื่อเขียนแล้วจะแปลเป็นภาษา Lua อีกทีแล้วรันด้วย LuaVM ดังนั้นเครื่องที่จะเล่นเจ้า Fennel ได้จึงต้องลง Lua ไว้ด้วย
แล้วจะใช้ไปทำไมเขียน Lua ไปเลยไม่ดีกว่าหรือง่ายอยู่แล้ว ผู้สร้างภาษาเขาเขียนที่มาที่ไปไว้แล้วแต่ถ้าจะให้ผมบอกก็คือเพื่อทำในสิ่งที่ Lua ทำได้แต่ยุ่งยากให้สะดวกขึ้นง่ายขึ้น เช่นภาษา MoonScript/YueScript ที่ปรับ syntax ให้กระชับ เขียนแบบ OOP ได้สะดวกขึ้น จัดการกับ table ได้ง่ายขึ้น หรือการลดข้อผิดพลาดในการเขียนโปรแกรมอย่างเช่นภาษา Teal ที่เป็น Lua ในแบบที่มีการกำหนด type ของตัวแปรให้ชัดเจนเป็นต้น

มารู้จักกับ Fennel

Fennel เป็นภาษาเชิงนิพจน์ที่มี syntax ในแบบของภาาษา Lisp แต่สร้างให้มีความเข้ากันได้กับ Lua ทำให้ไม่ต้องเปลี่ยนวิธีการเขียนมากนักและสามารถเรียกใช้ standard library และมอดูลต่างๆ ของ Lua ได้โดยตรงและในทางกลับกันก็สามารถเรียกใช้มอดูลที่เขียนด้วย  Fennel ใน  Lua ได้เช่นกัน ซึ่งต่างภาษาแนว Lisp ที่แปลงเป็น Lua ตัวอื่นอย่างเช่น Urn ที่มีความเป็น Lisp มากกว่าและมีมอดูลเป็นของตัวเอง
โดยส่วนตัวแล้วผมว่า Fennel เป็น Lisp ที่เรียนรู้ง่ายเพราะ minimal เหมือน Lua ไม่ต้องจดจำคำสั่งมากมาย ถึงจะเน้นภาษาเชิงฟังก์ชันเป็นหลักแต่ก็สามารถใช้โครงสร้างข้อมูลที่ซับซ้อนมีการผูก metatable ทำเป็น OOP เหมือน Lua ได้ สามารถใช้ลูปในการจัดการกับ table ได้ เข้าถึงสมาชิกใน table แบบ  Lua เช่น (table.key) กับ (table:method) หรือแบบ Lisp เช่น (. table key) กับ (: table method) ก็ได้

ข้อดีเมื่อเทียบกับ Lua

  • มี option --compile-binary แปลงเป็นไฟล์ไบนารี่ที่สามารถรันโปรแกรมได้โดยตรง ไม่ต้องอาศัยเครื่องมือช่วยแปลงไฟล์ตัวอื่น
  • เนื่องจากต้องแปลงเป็น Lua ก่อนทำงานจึงสามารถตรวจสอบความถูกต้องโปรแกรมในขั้นตอนการแปลก่อนได้
  • สามารถทำ macro เพื่อปรับแต่งโปรแกรมในขั้นตอนการแปลงได้
    
    (macro += [x n] '(set ,x (+ ,x ,n)))
    (var a 1)
    (+= a 2)                            ; เรียกใช้มาโครในตอนคอมไพล์จะแปลงโค้ดให้เหมือนกับ (set a (+ a 2))
    (print a)                           ; ได้ 3 ( a == 1 + 2 == 3 )
  • มีความชัดเจนกว่า Lua ป้องกันความสับสนช่วยลดข้อผิดพลาดได้ดีกว่า
    • มีตัวแปรทั้งแบบ mutable และ imutable
    • มีการแยก table แบบคีย์เป็นเลขลำดับเป็น array ใช้สัญลักษณ์เป็น [ ] ต่างกับ table ที่ใช้ { }
    • มี lambda ที่เป็นฟังก์ชันที่มีการตรวจสอบจำนวนอาร์กิวเมนท์กับพารามิเตอร์ให้ตรงกัน
  • operator เป็นแบบ prefix ทำให้ใช้ operator ตัวเดียวกับข้อมูลหลายตัวได้เข่น
    >>(+ 2 3 5)    ; เทียบเป็น Lua ก็จะเป็น  return 2+3+5
    10
    >>(>= 6 3 1)                       ; return (6 >= 3) and (3 >= 1)
    true
    >>(and x y z)                      ; return x and y and z
    nil
  • มี match pattern ที่นอกจากใช้แทน switch case แล้วยังสามารถจับคู่ค่าใน table กับตัวแปรใน pattern ได้ด้วย
    (let [case ["a" "b" "c"]]
      (match case
        [3 a b]   (+ a b)               ; ไม่ match "a" ไม่เท่ากับ 3
        [w x y z] "hello"               ; ไม่ match จำนวนสมาชิกใน array ไม่ตรงกัน
        [x "b" y] (.. y x)))            ; match "b" จับคู่ x = "a" และ y = "c"
    
    ;; ได้ผลลัพธ์เป็น "ca"
    หรือใช้กับชุดของข้อมูลหลายตัวก็ได้
    
    -- จากโค้ด Lua
    local f, error_msg = io.open "abc.txt"
    if f then
      print(type(f))
      f:close()                        -- จุดต่างคือ Lua ไม่มีการคืนค่าแต่ Fennel จะเหมือนกับ return f:close()
    else
      print(error_msg)                 -- จุดต่างคือ Lua ไม่มีการคืนค่าแต่ Fennel จะเหมือนกับ return print(error-msg)
    end
    
    
    ; เขียนเป็น Fennel ดังนี้
    (let [(f error-msg) (io.open "abc.txt")]
      (if f
          (do                           ; if จะมีแค่นิพจน์นิพจน์ที่เงื่อนไขเป็นจริงกับนิพจน์ที่เงื่อนไขเป็นเท็จอย่างละหนึ่งในแต่ละเงื่อนไขถ้าอยากให้มีหลายนิพจน์ต้องใช้ (do ) ครอบ ( เหมือน do end บล๊อกในใน Lua )
            (print (type f))
            (f:close))
          (print error-msg)))
    
    ; สามารถใช้ match ในการจับคู่ตัวแปรแทนได้
    (match (io.open "abc.txt")
      (nil error-msg) (print error-msg) ; ถ้าค่าที่ส่งกลับมาจาก io.open มีหลายค่าและค่าแรกเป็น nil ให้พิมพ์ค่าที่สอง
      f               (do               ; ถ้ามีค่าเดียวให้พิมพ์ประเภทของค่านั้นจากนั้นให้ปิดไฟล์
                        (print (type f))
                        (f:close)))
    
  • มี destructuring เหมือนใน JavaScript
    
    (let [[a b]       [1 3]             ; a = ({1, 3})[1] และ b = ({1, 3})[2]
          {:x x2 : y} {:x 2 :y 6}]      ; x2 = ({x = 2, y = 6}).x และ y = ({x = 2, y = 6}).y
      (print (+ a b))                   ; ได้ 4 ( 1+3 )
      (print x2 y))                     ; ได้ 2   6
  • ตัวเลขสามารถแทรก underscore เพื่อให้อ่านง่ายเช่น 1_932_490
  • สามารถแทรกข้อความเป็น docstring ในฟังก์ชันได้
  • มี table comprehension เพิ่มความสะดวกในการสร้าง table ใหม่จาก table เดิม
    
    -- ในภาษา Lua เอาเฉพาะเลขคู่มาสร้าง table ใหม่เราใช้ลูปธรรมดา
    local t1 = {1, 2, 3, 4, 5, 6}
    local t2 = {}
    for i, v in ipairs(t1) do
      if v % 2 == 0 then
        t2[#t2+1] = v
      end
    end
    -- หรือใช้มอดูลเสริม
    -- ซึ่งก็จะแปลงกลับเป็นลูปธรรมดาแต่มี overhead จากฟังก์ชันแค่ให้เขียนโค้ดง่ายขึ้น
    -- แต่ไม่มีผลด้านประสิทธิภาพดีขึ้นแต่อย่างได
    local comp = require("pl.comprehension").new()
    local t3 = comp "x for x if x % 2 == 0" (t1)
    
    -- MoonScript จะคล้ายใน Python
    t1 = [1, 2, 3, 4, 5, 6]
    t2 = [x for x in *t1 when x % 2 == 0]
    
    ; ใน Fennel การใช้ลูปปกติ
    (let [t1 [1 2 3 4 5 6]              ; do local t1 = {1, 2, 3, 4, 5, 6}
          t2 []]                        ;   local t2 = {}
      (each [_ v (ipairs t1)]           ;   for _, v in ipairs(t1) do
        (when (= (% v 2) 0)             ;     if v % 2 == 0 then
          (tset t2 (+ (length t2) 1) v))) ;     t2[#t2 + 1] = v end--if
                                        ;   end--for
      t2)                               ;   return t2
                                        ; end--do
    
    ; สามารถใช้ collect กับ icollect แทนได้
    ; แบบ icollect ใช้กับ value
    (let [t1 [1 2 3 4 5 6]
          t2 (icollect [_ v (ipairs t1)]
                (when (= (% v 2) 0) v))] ; คืนค่า v ตัวเดียว
      t2)
    
    ; แบบ collect ใช้กับคู่ key value
    (var i 0)
    (let [t1 [1 2 3 4 5 6]
          t2 (collect [_ v (ipairs t1)]
                (when (= (% v 2) 0)
                  (set i (+ i 1))
                  (values i v)))]       ; คืนค่าเป็นคู่ลำดับ i, v
      t2)
    ; จะได้ผลลัพธ์เหมือนกันแต่การใช้ table comprehension จะเขียนง่ายกว่าการใช้ลูป
    
    ; กรรณีที่เป็นการ copy หรือ slice table สามารถใช้การ destruct ได้
    (var t1 [1 2 3 4 5 6])              ; t1 == [1 2 3 4 5 6]
    (var [& t1-copy] t1)                ; t1-copy == [1 2 3 4 5 6]
    (var [head & tail] t1)              ; head == 1, tail == [2 3 4 5 6]
  • มี nil-safe table lookup เช่น
    -- ในภาาษา  Lua
    local t = {1, {a = 2, b = {3, 4}}}
    print(t[2].b[1])                   -- ได้ 3
    print(t[2].c)                      -- ได้ nil เพราะไม่มี index "c"
    print(t[2].c[1])                   -- error เพราะ "c" เป็น nil ไม่ใช่ table ไม่มี index
    
    ;; ในภาาษา Fennel
    (. t 2 :c 1)                        ; error เหมือน Lua
    (?. t 2 :c 1)                       ; ได้ nil ไม่ error
  • ทุกอย่างคือนิพจน์เงื่อนไขก็เป็นนิพจน์
    (var a true)
    (set a (if a false true))
    
     เทียบเป็น lua
    local a = true
    if a then
      a = false
    else
      a = true
    end
  • มีมาโครเสริมเช่น doto macro และ threading macros สำหรับจัดลำดับการทำงานได้แก่ " -> ", " ->> ", " -?> ", " -?>> "
    
    ;; เช่นการใช้ A and B or C ใน Lua เมื่อเป็น Fennel จะเป็น
    (or (and A B) C)
    ;; แต่เมื่อใช้ -> จะเป็น
    (-> A (and B) (or C))
    ;; จะเห็นว่าอ่านง่ายกว่าเหมาะสำหรับจัดการกับการใช้กับฟังก์ชันซ้อนๆ กันหลายชั้น
    ;; ส่วน ->> จะกลับกันเอาค่าไปแทนส่วนท้าย
    (->> B (and A) (or C))
    ;; จะได้เป็น
    (or C (and A B))
    ;; ส่วน -?> และ -?>> จะเพิ่มการสรวจสอบค่า nil ด้วย
    
    ;; doto จะคล้าย with ในภาษาอื่นเช่น
    (tset very-very-long-table-name :key1 value1)
    (tset very-very-long-table-name :key2 value2)
    (tset very-very-long-table-name :key3 value3)
    ;; สามารถเขียนให้สั้นขึ้นด้วย
    (let [x very-very-long-table-name]
      (tset x :key1 value1)
      (tset x :key2 value2)
      (tset x :key3 value3))
    ;; หรือใช้ doto แทน
    (doto very-very-long-table-name
      (tset :key1 value1)
      (tset :key2 value2)
      (tset :key3 value3))

Fennel เป็นภาษาเชิงนิพจน์

นิพจน์ใน Fennel ประกอบไปด้วยข้อมูล ( ภาษาตระกูล Lisp เรียกว่า atom ) หรือลิสต์ของข้อมูล โดยที่ข้อมูลตัวแรกจะเป็นฟังก์ชันและตัวที่เหลือจะเป็นอาร์กิวเมนท์ของฟังก์ชันนั้น ( ซึ่งก็คือ function call นั่นเอง (print 1 2 3 "a") ---> print(1, 2, 3, "a"), (myfunc) ---> myfunc() ) ซึ่งแต่ละอาร์กิวเมนท์จะถูกประมวลผลก่อนส่งเข้าฟังก์ชันยกเว้น special form ที่มีลักษณะต่างออกไปเช่น (if condition value1 value2) จะเป็นการตรวจสอบ condition ถ้าจริงจะคืนค่า value1 โดยไม่มีการประมวลผลในส่วนของ value2 และถ้าเป็นเท็จจะคืนค่า value2 โดยข้ามการประมวลผลในส่วนของ value1 เป็นต้น
ในส่วนของฟังก์ชันและบล๊อกจะคืนค่านิพจน์สุดท้ายเสมอ Fennel ไม่มี early return และไม่มี break แต่สามารถใช้ lua escape hatch มีรูปแบบดังนี้ (lua "Lua expression") เช่น

(fn test-lua [n]                        ; function test_lus(n)
  (for [i 1 10]                         ;   for i = 1, 10 do
    (when (> i n)                       ;     if i > n then
      (lua "return i"))                 ;       return i end--if
    (print (.. "i = " i)))              ;     print("i = "..i) end--for
  (.. n " >= 10"))                      ;   return n .. " >= 10" end--function

(test-lua 10)                           ; พิมพ์ "i = 1" ไปจนถึง "i = 10" แล้วคืนค่า "10 >= 10"
(test-lua 5)                            ; พิมพ์ "i = 1" ไปจนถึง "i = 5" แล้วคืนค่า "6"
แต่ในรุ่นล่าสุด ( 0.9 ) ได้เพิ่ม :until สำหรับการออกจากลูปมาด้วยเช่น

(fn test-until [n]                      ; function test_until(n)
  (var res 0)                           ;   local res = 0
  (for [i 1 10 :until (> i n)]          ;   for i = 1, 10 do if i > n then break end--if
    (set res i)                         ;     res = i
    (print (.. "i = " res)))            ;     print("i = "..res) end--for
  (if (< res 10)                        ;   if res < 10 then
        (+ res 1)                       ;     return res + 1
      (.. n " >= " res)))               ;   else return (n .. " >= " .. res) end--if
                                        ; end--function

(test-until 10)
(test-until 5)                          ; ผลลัพธ์เหมือนฟังก์ชันข้างบน

;; ตัวอย่างการโดดออกจากลูปแบบต่างๆ
; การใช้ while
(fn check-win1 [p]                      ; function check_win(p)
  (var (result i) (values true 1))      ;   local result, i = true, 1
  (while (<= i (length win))            ;   while i <= #win do
    (set result (all p (. win i)))      ;     result = all(p, win[i])
    (if result                          ;     if result then
          (set i (+ (length win) 1))    ;       i = #win + 1
        (set i (+ i 1))))               ;     else i = i + 1 end--if
                                        ;   end--while
  result)                               ;   return result end--function

; การเขียนฟังก์ชันแบบ recursion
(fn check-win2 [p w k r]                ; function check_win2(p, w, k, r)
  (var (key line) (next win k))         ;   local key, line = next(win, k)
  (if (or (not w) (and key (not r)))    ;   if (not w) or (key and (not r)) then
        (check-win2 p line key (all p line)) ; return check_win2(p, line, key, all(p, line))
      r))                               ;   else return r end--if
                                        ; end--function

; การใช้ :until ในลูป
(fn check-win3 [p]                      ; function check_win3(p)
  (var result false)                    ;   local result = false
  (each [_ v (ipairs win) :until result] ;   for _, v in ipairs(win) do if result then break end--if
    (set result (all p v)))             ;     result = all(p, v) end--for
  result)                               ;   return result end--function

; การใช้ lua escape hatch
(fn check-win4 [p]                      ; function_win4(p)
  (var result false)                    ;   local result = false
  (each [_ v (ipairs win)]              ;   for _, v in ipairs(win) do
    (set result (all p v))              ;     result = all(p, v)
    (when result (lua :break)))         ;     if result then break end--if
                                        ;   end--for
  result)                               ;   return result end--function
; หรือ
(fn check-win5 [p]                      ; function check_win5(p)
  (each [_ v (ipairs win)]              ;   for _, v in ipairs(win) do
    (when (all p v) (lua "return true"))) ;     if all(p, v) then return true end--if
                                        ;   end--for
  false)                                ;   return false end--function
เห็นได้ว่าแม้จะเป็นภาษารูปแบบของ Lisp แต่การเขียนไม่ต่างจาก Lua ปกติมากนักคนที่เป็น Lua มาแล้วก็สามารถหัด Fennel ได้ไม่ยากนักตัวแปลภาษาเขียนด้วยภาษา Lua และตัว Fennel เองดังน้้นไม่ต้องติดตั้งอะไรเพิ่มเติมสามารถนำไปใช้ในโปรเจคเดิมได้ง่ายไม่ต้องปรับเปลี่ยนอะไรมากจะรันโปรแกรมด้วย Fennel เองเลยหรือจะคอมไพล์เป็นไฟล์ Lua แล้วเอาไปรันใน Lua อีกทีก็ได้ หรือจะให้ไฟล์ Lua โหลดมอดูล Funnel ก็ได้เช่นกันถ้าสนใจแล้วก็ลองไปเล่นดูได้ครับถ้าใครมี luarocks อยู่แล้วก็แค่
$ luarocks install fennel
หรือใครที่ลงโปรแกรม Tic-80 ที่ใช้สร้างเกม 8 บิทไว้แล้วก็สามารถใช้ Fennel ได้เลย ( แค่ไม่ใช่ Fennel รุ่นล่าสุด ( ใน Tic เป็นรุ่น 0.6.1-dev ) syntax บางตัวอาจไม่เหมือนกันหรือไม่มีในรุ่นเก่ากว่า ) แค่พิมพ์ new fennel ( ถ้า new เฉยๆ จะเป็น Lua ) ที่คอนโซลของโปรแกรมครับ สำหรับบทความนี้ก็ขอจบเพียงเท่านี้สวัสดีวันสงการณต์ล่วงหน้าครับ 😄

เอารูปผักชีล้อมมาฝาก

ความคิดเห็น

  1. กำลังสนใจภาษา Fennel พอดีค่ะ ว่าจะหัดเขียนเล่นดู ปกติเขียนด้วย MoonScript ค่ะ

    อ้อ ตอนนี้ TIC-80 อัพเดตเป็น Fennel เวอร์ชั่นล่าสุด (1.3.0) แล้วค่ะ

    ตอบลบ
    คำตอบ
    1. MoonScript ก็สนุกดีครับถ้าถนัด OOP สร้างคลาส Fennel จะไปทาง functional ถ้าจะสร้าง object ต้องจัดการ table เองไม่ก็โหลดโมดูลภายนอกมาช่วย

      ลบ

แสดงความคิดเห็น