ลอง Mithril.js

หลังจากเขียน Vue ไปเกือบเสร็จ มารู้ว่า Vue จะทำ hook โดยเฉพาะที่ Vue 3 อยากจะเปลี่ยน API เป็น function หมดแล้วโดนด่าต้องถอยสุดซอย การันตีว่าจะไม่เปลี่ยนอีกใน Vue 3.x (แล้ว 4 ล่ะ?) ก็เลยว่าจะ rewrite

สรุปว่า Project นี้เปลี่ยน framework ไปแล้ว 3 ตัว คือ Svelte (รีวิวไว้ใน Facebook ตั้งแต่ Svelte 3 ออก), Vue และ Mithril

Mithril นี่ claim มันคือมันบอกว่า API design มันคิดมาถูกตั้งนานแล้ว React อ่ะ design มั่วถึงต้องเปลี่ยนไปเปลี่ยนมา

แถม bundle size เล็กมาก 10kB เท่านั้น แล้วไม่ใช่แค่ view library อย่างเดียวแต่ใน 10kB นี้รวม router, promise polyfill และ XHR library แล้วด้วย!! แล้วเค้าก็ขายต่อว่า เพราะโค้ดมันเล็ก มันเลย review ได้ง่ายว่าปัญหาอยู่ตรงไหน performance ก็จะดีตาม

ส่วนว่าทำไม Mithril มีมาตั้งแต่ 6 ปีแล้วแต่ไม่ดัง บางคนบอกว่า “อ๋อ คนเขียน Mithril เค้าไม่ว่างเชียร์ พอดีกำลังเพลิดเพลินกับการเขียนโค้ดอยู่”

เอาสิ คนเขียน Vue ยังไม่ขิงขนาดนี้อ่ะ

Reactivity

View library ยุคใหม่ๆ จะเป็น Declarative หมด ก็คือเราบอกว่า interface หน้าตาแบบนี้ แล้ว library จะไปหาทาง update UI บนหน้าจอให้ตรงกับสิ่งที่เราบอกเอง

แต่สิ่งที่แสดงบนหน้าจอมักจะ depend กับ state เมื่อ state เปลี่ยนต้อง rerender interface ใหม่ ซึ่งวิธีที่แต่ละ Library ทำก็จะแตกต่างกันออกไป…

  • AngularJS 1 ใช้ dirty checking โดยมันจะแอบ clone state เก่าไว้ พอเราแก้มันจะ diff state เก่ากับใหม่เพื่อดูว่า state ตรงไหนเปลี่ยน แล้ว view ตรงไหน depend กับ state นี้
  • MobX 4 กับ VueJS 2 ใช้วิธี rewrite ทุก field บน state object ให้มี getter setter เพื่อที่จะได้ track การใช้งานได้ว่ามีการ set field นี้ แล้วใคร get มันบ้าง
  • MobX 5 กับ VueJS 3 ใช้ Proxy แทนการ rewrite field
  • Svelte ใช้ compiler rewrite field assignment ให้ไป mark ว่า field นี้มีการเปลี่ยนแปลง
  • React บังคับให้ state เป็น Immutable เพื่อให้ set state ได้ทางเดียว เมื่อ set แล้วก็จะ rerender component ที่เปลี่ยน state เพราะไม่รู้ว่าตรงไหนใช้อะไรบ้าง
  • Redux ใช้ Immutable state เช่นกัน เมื่อมีการ dispatch action แล้ว listener จะยิงให้ view library refresh

คำตอบของ Mithril คือ Mithril ไม่มี reactivity

ถ้าใครเขียน AngularJS 1 หรือ MobX จะรู้ว่าการเปลี่ยนค่าตัวแปรจะต้องรันในฟังค์ชั่นที่อนุญาตเท่านั้น ไม่อย่างนั้นมันอาจจะไม่อัพเดต ใน angular เรียกว่า $apply หรือ MobX เรียกว่า action เมื่อออกจาก action แล้ว library จึงจะเช็คว่าต้องแจ้งใครให้อัพเดตตามบ้าง

ใน Mithril โค้ดก็จะคล้ายๆ กัน แต่ไม่ต้องครอบฟังค์ชั่น เพียงแค่เรียก m.redraw() หลังจากแก้ state เสร็จแล้วเท่านั้นเอง แล้วมันจะ rerender ทั้งหน้าเว็บหลัง requestAnimationFrame

แล้วจุดที่มักจะมี state change ใน Angular เองก็จะครอบ $apply ให้ ใน Mithril ก็จะทำให้เช่นกัน ได้แก่ event handler และ หลัง AJAX call (ถ้าใช้ m.request)

ผมไม่แน่ใจว่าผมรู้สึกยังไงกับฟีเจอร์นี้ การทำ fine grained update แบบ Angular อาจจะฟังดูเร็วกว่าแต่มันก็มี CPU overhead และเพิ่ม code size ที่ต้อง track change ในขณะที่การรีเฟรชทั้งหมดก็เปลือง CPU

Global

State ของ Mithril เป็น Global เกือบทั้งหมดจนผมกังวลใจ

เช่น ฟังค์ชั่น m.redraw() ที่เล่าไปด้านบน นั่นคือ Signature เต็มๆ ของมันแล้วเพราะ Mithril เก็บ state เป็น global ไม่ต้องบอกว่าจะ redraw อะไร

หรือ Mithril router เองก็ใช้ m.router.get() อ่านค่า Route ล่าสุดโดยไม่ต้องพยายามหา router instance

เรื่องนี้คิดว่าเป็นเรื่องหนึ่งที่ยากจะกลืน พอๆ กับที่ React เอา HTML กับ JavaScript ผสมกัน (คนเขียนเว็บยุคเก่าจะบอกว่ามันผิด Separation of Concern) แต่คิดว่าที่ออกแบบมาแบบนี้เพราะ Mithril ไม่ได้คิดเรื่อง server side rendering เท่าไรนัก โค้ดที่ใช้ทำ SSR เองก็อยู่นอก codebase หลักของ Mithril

ผมก็เลยต้องถามว่าแล้วมันจะ get router state บน server ยังไงให้มัน thread safe เพราะถ้ากำลัง fetch data อยู่แล้วอีก request เข้ามา route มันก็จะเปลี่ยน

ปรากฎว่า ใน project ตัวอย่างมันใช้ Mithril router ใน clientside อย่างเดียว บน server มัน convert route definition ไปรันบน Express router

ก็เป็นประเด็นหนึ่งที่น่ากังวลถ้าต้องทำ server side rendering เพราะมันทำได้ แต่อาจจะต้อง if แยกไปพอสมควร สำหรับใน project นี้แล้วไม่มี server side rendering ก็เลยยังไม่ต้องห่วงเท่าไร

Component

เล่าทฤษฎีเสียนาน เข้าประเด็นดีกว่าว่า Component ใน Mithril หน้าตาอย่างไร

Component ใน Mithril เป็น JavaScript Object ธรรมดา ซึ่งก็น่าสนใจดีเพราะมันไม่ใช่ class ที่จะมี constructor และมันไม่ใช่ function ที่จะไม่มี state

const component = {
    view() {
        return m(".text", ["Hello world"]);
    }
}

ในทางปฏิบัติเหมือนว่า Mithril จะทำให้ component มี state อยู่ดีและเราสามารถเก็บข้อมูลไว้ใน this ได้ หรือใช้เรียกเมธอดอื่นๆ บน component

ฟังค์ชั่น JSX ของ Mithril คือ default export ของ Mithril เองซึ่งมักจะนิยมตั้งเป็น m

import m from 'mithril';

แต่ Mithril ไม่ค่อยจะเชียร์ JSX เท่าไรนักนอกจากบอกว่ามันก็ทำได้ เพราะ m จริงๆ นอกจากจะระบุ parameter แรกเป็น HTML Tag หรือ component ได้แล้วมันยังสามารถระบุ CSS Selector ได้ด้วย เรียกว่า Hyperscript เช่น m("input.input-lg[type=text][name=username]") จะได้ผลเป็น <input class="input-lg" type="text" name="username"> (กรณีที่มี dynamic attribute ก็ยังใส่ใน parameter ที่ 2 ของ m ได้ตามปกติเหมือน JSX ทั่วไป)

การใช้งานจริงผมพบว่า Prettier ไม่เข้าใจ Hyperscript และทำให้โค้ดอ่านค่อนข้างยาก เช่น โค้ดนี้

const component = {
    view() {
        return m("form.main", {
            onsubmit: onSubmit,
        }, [
            "Username: ",
            m("input[type=text][name=username]"),
            m("br"),
            m("input[type=submit][value=Submit]"),
        ]);
    }
}

จะโดน Prettier จัดเป็น

const component = {
    view() {
        return m(
            "form.main",
            {
                onsubmit: onSubmit,
            },
            [
                "Username: ",
                m("input[type=text][name=username]"),
                m("br"),
                m("input[type=submit][value=Submit]"),
            ]
        );
    },
};

ทำให้โค้ดค่อนข้างเทอะทะ อ่านยาก ทาง Prettier เองก็ไม่อยากให้มี Framework-specific formatting เท่าไรแต่อาจจะมีระบบ Plugin ในอนาคต

ในขณะเดียวกันเองถ้าจะเลี่ยงปัญหานี้ไปเขียน JSX เลย ก็ยังรู้สึกว่า m syntax สะดวกในการเขียนมากกว่า อย่างหนึ่งคือมันแยก static attribute กับ dynamic attribute ออกจากกันชัดเจน ก็ยังตัดสินใจไม่ค่อยได้เท่าไรว่าใช้แบบไหนดี ไม่อยากผสมกันหลายๆ ท่าเพราะมันจะเหนื่อยตอน convert เมื่อ component เริ่มเปลี่ยนไปแล้วอีกแบบเหมาะกว่า

Local State

ผมดองบล็อคเรื่องว่า React hook มันผิดยังไงมานานแล้ว สั้นๆ คือ Function ไม่มี State แต่ปรากฏว่า React hook call แต่ละครั้งได้ผลไม่เหมือนกัน แปลว่ามันต้องมี Global state สักที่

แล้ว React ก็ใส่ magic เข้าไป ทำให้ state แทนที่จะเป็น Global ก็ขึ้นอยู่กับ call site ได้ แต่เมื่อ call site เปลี่ยนมันก็พัง รวมถึงไปเรียกนอก React ไม่ได้

คำถามคือ แล้ว useState() ที่ไม่ Magic หน้าตายังไง? ผมก็ไม่รู้เหมือนกัน จนกระทั่งมาเห็นท่าที่ Mithril ทำ

function factory() {
    let counter = 0;

    return {
        view() {
            return m("div", [
                counter,
                m("input[type=button]", {
                    onclick: () => {
                        counter++
                    }
                })
            ])
        }
    }
}

ใช่! ใช่! ใช่!

วิธีที่เป็น Idiomatic ของ JavaScript ในการทำ local state ก็คือ Closure กับ class attribute นี่แหละ

เวลาใช้งาน function นี้เราไม่ต้องเรียก factory function เองแต่ Mithril ยอมรับ component factory เป็น component ด้วย แล้วมันจะไป manage instance ให้เอง

Final Thoughts

ผมคิดว่า Mithril ยังไม่ตอบโจทย์อยู่ 2 เรื่องที่ framework อื่นๆ ถือเป็นเรื่อง Hot มาก คือ Server rendering ซึ่งตามมาด้วยประเด็นว่า Global state ใน Frontend มันกลายเป็นอดีตไปแล้วเมื่อมันต้อง render บน client side

ถ้าตัด Server render ออกแล้วใช้ Global state คิดว่ามันก็น่าจะใช้ง่ายดี อาจจะตัด state management library ไปได้ด้วย แต่ถ้าทำแบบนั้นจะเอาไปเทียบกับเฟรมเวิร์คอื่นๆ ก็จะไม่แฟร์เท่าไร

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

Hard things in VDOM Framework

หลังจากดู Framework มาหลายตัว คิดว่าจุดที่ควรจะศึกษาจาก Framework แต่ละตัวคือ

Routing

เท่าที่เขียนเว็บมา Routing จะมีอยู่สามแนวคือ

  1. Router เป็น component ที่ render subtree ตามหน้า ท่านี้จะเหมือนกับ React Router 4
  2. Router เป็น state manager แล้วเราต้อง check state และ render เอง ท่านี้เจอใน Router5
  3. Router อยู่ข้างนอกแล้ว Manage root component ตามที่ใช้ อันนี้เคยเขียนเองใน abstract-state-router หรือ Mithril router ก็จะทำท่านี้

ตอนนี้ยังเอนเอียงอยู่ระหว่าง 2 กับ 3 ว่าท่าไหนเป็นท่าที่ถูก ข้อดีของ Router5 คือมันทำ subroute แล้วให route หลัก render ส่วนนึง subroute render อีกส่วนได้ แต่ใน Vue อาจจะใช้ Slot API ทำท่านี้แทนได้โดยไม่ต้องพึ่ง router?

อีกปัญหาหนึ่งที่ยากของ Router คือ Fetch data ก่อนที่จะ Navigate ซึ่งเหมือนกับที่ Browser ทำอยู่ ก็แปลกใจมากที่ React Router 4 เลิก support ท่านี้ไปแล้วให้ไปทำที่ Link component แทน (กลายเป็นว่าคนใช้ต้องรู้ว่าจะ link ไปหน้านี้ต้องใช้ component ไหน) คิดว่า React suspense ออกมาเพื่อแก้ปัญหานี้ ส่วน Framework อื่นๆ ยังไม่ได้สนใจตรงนี้

State management

State ใน web framework เท่าที่เห็นจะมีอยู่สามแบบคือ

  1. Local state เช่น form เพราะถ้า hoist มันขึ้นมันจะช้า (แต่ก็มีคนทำอยู่บ้าง พวก Redux form)
  2. Scope state คือ local state แต่ pass through ลงไปให้ component ลูกด้วย บางที scope อาจจะใหญ่มากจนถึงทั้งหน้า
  3. Global state คือ state ที่ share ระหว่างหน้า หรือระหว่าง component ที่ไม่ได้เป็น parent-child กัน

ที่น่าจะดูคือเราสามารถ Pass object จากด้านบนลงไปด้านล่างแบบ implicit ได้ไหม (แบบ React context API) เพราะการใช้ global variable ไม่ดีต่อ server render ในขณะเดียวกันการ pass ทุกอย่างเป็น explicit ก็ทำให้ component ที่ไม่อ่าน state ก็ต้องรับ state ไปเพื่อส่งต่อด้วย (นอกจากนั้นอาจจะเจอปัญหาว่าข้างบนไม่ได้รับ state ข้างล่างจะใช้ไม่ได้)

บาง Framework อาจจะมี state management มาในตัวเลย (หรือมีตัวที่ค่อนข้างแนะนำ) อันนี้ก็ต้องลองดูเหมือนกันว่ามันเวิร์คมั้ย เช่น Angular $scope หรือ Vuex (ที่ไม่ได้มาด้วยกันแต่แนะนำมากๆ)

Reactivity

พูดถึง state แล้วสิ่งที่ตามมาคือเมื่อ state เปลี่ยนแล้วมันจะรู้ได้ยังไงว่าต้อง rerender อันนี้ด้านบนอธิบายไปแล้ว เลื่อนขึ้นไปอ่านได้

Code Splitting

อย่างน้อยๆ ก็ใน Router ก็มักจะได้ใช้ Code splitting แล้วใน Page อาจจะแยกต่อไปอีกก็ได้

บาง Framework เช่น Vue รองรับ component ที่คืน Promise มาได้เลย เห็นว่า React Suspense ก็น่าจะมา ส่วนบาง library ที่บ้านๆ มาหน่อยอาจจะต้อง manage Promise state เอง

ฟังดูเหมือนไม่ยาก แต่ถ้าวาด state machine มาจะพบว่ามันมีหลาย state กว่าที่คิด

  1. Initial (บาง framework เช่น React side effect จะเกิดขึ้นได้เฉพาะหลัง mount แล้ว)
  2. Loading แต่ไม่แสดง indicator
  3. Loading และแสดง indicator (ปกติถ้าโหลดเร็วมากๆ ไม่ขึ้น indicator เลย คนจะรู้สึกว่าเว็บเร็วกว่า)
  4. Load สำเร็จแล้ว ไปแสดง component จริง
  5. Load ไม่สำเร็จ แสดง error component

และบน server render มันอาจจะต้อง force ให้มันโหลด component ทันทีก่อนคืนให้ client

Fragment

บางที component ย่อย/output ของ loop อาจจะจำเป็นต้องคืนสมาชิกหลายตัวโดยไม่มีอะไรครอบ โดยเฉพาะเมื่ออยู่ภายใต้ <table> หรือ <ul> ตัว framework ควรจะอนุญาตให้คืนหลายๆ child component ได้

หรือบางที component อาจจะไม่อยาก render เลยถ้าไม่มีอะไร เช่น error display ถ้าไม่มี error ก็ไม่ต้องคืนอะไร

บาง library เช่น domvm ใช้แนวคิดว่า virtual DOM map 1-1 กับ DOM จริง ก็เลยจะไม่ support use case ข้างบนนี้ ต้องทำ div ขยะไว้บนหน้าแทน

Portal

โดยปกติแล้วในเว็บเราจะใช้ element ซ้อนกันไปได้เรื่อยๆ แล้วก็ใช้ CSS จัดหน้าได้ไม่มีปัญหา แต่ CSS เช่นกันที่ทำให้เราซ้อน element ที่ไม่เกี่ยวข้องกันไม่ได้เพราะ style ของ parent มีผลต่อ child ด้วย และบางอย่าง child override ไม่ได้ เช่น z-index ที่ยุ่งยาก ไปจนถึง position: relative แล้วมี position: absolute ซ้อนด้านใน

requirement ที่จะเจอบ่อยมากคือ Modal window ซึ่งในแอพง่ายๆ อาจจะยังไม่เจอหรือขี้เกียจเขียนแล้วใช้ prompt/alert ไปก่อน แต่พอจะทำดีๆ แล้ว framework บางตัวไม่มีคำตอบให้ว่าตอนนี้อยู่ข้างในจะโผล่ Modal ออกมาได้ยังไง

ท่า hackๆ หน่อย ก็อาจจะเป็นว่ามี component ตัวรับอยู่ด้านนอกแล้ว component ด้านในส่งสัญญาณไปหาตัวนอกว่าช่วย render อันนี้ตรงด้านนอกให้หน่อย ซึ่งมัน break การแยก component (ทำไม component ตัวนอกต้อง add portal receiver ที่ตัวเองไม่ได้ใช้) แล้วอาจจะมี hack เข้ามาเกี่ยวข้อง (component ด้านในคุยกับตัวด้านนอกยังไง? แล้วมันส่ง vdom ออกไปได้ยังไง?)

คำถามถัดมาคือแล้ว event bubbling จะทำงานยังไง? ถ้า parent ดัก click event ทั้งหมดไว้แล้วเราคลิกบน portal, parent ควรจะได้ไหม? เพราะ DOM จริงไม่ส่งมาเพราะมันไม่ได้เป็น parent-child กันแล้ว แต่ใน Virtual DOM มันยังเป็น parent-child กันอยู่นะ

CSS Class

คิดว่าการผสม CSS Class เป็นอะไรที่ใช้บ่อยมากถ้าใช้ CSS Module บาง framework เช่น Vue ก็ทำได้เลย แต่ถ้าใช้ React หรือ Mithril ก็ยังต้องใช้ classnames helper

บางคนก็จะใช้ CSS-in-JS ไปเลยซึ่งเวลาสั่งให้มันผสมมันจะทำ class ใหม่มา

Error Boundary

สุดท้ายเรื่อง Error ก็สำคัญกับ UX ที่ดี เราควรจะทำให้เว็บ partial failure ได้ว่าถ้า component ด้านในพัง ด้านนอกจะ render error ให้ user ได้ ซึ่งเราไม่สามารถ try..catch ใน render function ได้ (เพราะเราคืน Virtual DOM nodes ไปก่อน แล้ว vdom library เป็นคนเรียก component ด้านใน) ดังนั้น Virtual DOM ต้องจัดการเรื่อง error ให้ด้วยว่าถ้าข้างใน fail ต้องกลับมาให้ด้านนอก catch ได้อยู่

Programming Languages I Write

ตอนนี้เขียนโปรแกรมอยู่หลายภาษา ก็คิดว่าอยากลองเขียนดูบ้างว่าเราคิดว่าจุดแข็งของแต่ละภาษาคืออะไร แล้วเราเลือกใช้มันยังไง

กฎส่วนตัวเราเวลาบอกว่าเขียนภาษานี้ “เป็น” พอจะใส่ใน Resume คือถ้า Ecosystem ในเรื่องนั้นมันพร้อมเราสามารถทำ Task อะไรก็ได้ในภาษานั้นๆ และเขียนออกมา Idiomatic เฉพาะตัวแบบที่ภาษานั้นๆ เขียนอยู่ด้วย

หลักๆ ที่เขียนเลยก็จะมี

  • Python
  • Go
  • JavaScript & TypeScript

ภาษาอื่นๆ ที่เคยเขียนก็จะมี Java, C, C++, Rust, Kotlin, PHP แต่ในบทความนี้คงจะไม่พูดให้ครบทุกอัน

Python

Rule of thumb ตอนนี้คือถ้ามันจะต้องต่อ database เราจะเลือกใช้ Python และ Django เท่านั้น ไม่มีอะไรที่ทำให้เราทำงานเสร็จเร็วเท่า Django อีกแล้ว

นอกจาก Django แล้ว Python ฝั่ง Data Science ก็ค่อนข้างแข็ง แต่เราไม่ค่อยได้ใช้เท่าไร แล้วก็ยังทำ scripting ต่างๆ ได้เร็ว

ข้อเสียของ Python คือมันเป็นภาษาที่ปวกเปียกมาก

Package manager มันอยู่ระหว่าง requirements.txt ที่ใช้ได้แต่ค่อนข้าง manual และไม่มี lock กับ Poetry ที่ยังไม่ดัง (และคนไปเลือกใช้ Pipenv ที่เลิกทำแล้วมากกว่า) และ Virtualenv มันค่อนข้าง Hack หน่อยๆ เมื่อเทียบกับภาษาใหม่ๆ ที่เลิกมี global package repository แล้ว

Import syntax ที่ถ้ามีไฟล์อยู่ folder ข้างๆ จะนั่งงงมากว่าต้อง import ยังไง (มันไม่มี “absolute” import มีแต่ relative โดยใช้ syntax แปลกๆ)

Type checking ที่ยังตั้งไข่มากๆ ถึงจะทำมาหลายปีแล้ว และ syntax ที่มันแปลกๆ หน่อย

Speed ของมันที่ค่อนข้างช้าในการ execute ซึ่งปกติก็จะไม่ค่อยใส่ใจ แต่ก็มีครั้งนึงที่ทำให้พลาดข้อใน Codejam มาแล้วเพราะโค้ดเหมือนกันรันใน Python เกิน 5 นาที ไม่ผ่าน ไปเขียนใหม่ใช้วิธีเดิมใน C รันจบใน 30 วินาที

Web server deployment มันที่ยังต้อง deploy WSGI container เหมือนภาษาโบราณๆ ไม่ใช่ one click และก็จะติดเรื่อง thread pool ที่ต้องมาจูน

JavaScript

ผมไม่ชอบ JavaScript เท่าไรนัก แต่จำเป็นต้องเขียนเพราะมันใช้ได้หลายที่มากๆ มันเป็นภาษาเดียวที่รันได้ใน Browser เวลาจะทำอะไรที่เขียนครั้งเดียวใช้ได้ทุกที่ต้องใช้ JavaScript เลย

ข้อดีของ JavaScript คือ syntax มันค่อนข้างมาตรฐานไม่หวือหวา แล้วพอบวก TypeScript มามันทำให้การ code project ใหญ่ๆ ทำงานงายขึ้นเพราะ editor จะฉลาดขึ้นเยอะ

ข้อเสียของ JavaScript คือภาษามันค่อนข้างฟรีสไตล์และมันไม่เก่ง Class กลายเป็นว่าคนชอบเขียนเป็น functional แต่มันก็ไม่ใช่ภาษาที่ออกแบบเป็น functional มาตั้งแต่แรก โค้ดมันก็เลยเละมาก

แล้วที่ผมไม่ชอบ functional ใน JavaScript มากคือการที่เราเอา higher order function ที่ add functionality ทีละนิด แต่ใช้ซ้อนกันเยอะๆ เวลาไล่โค้ดมันจะงง flow มากว่ามันเข้าแล้วอะไรโผล่เข้ามาตอนไหน รวมถึง syntax มันที่จะเป็น a(b(c(d))) ซึ่งไม่ค่อยเป็นธรรมชาติในการอ่านเพราะ functionality หลักมักจะอยู่ที่ d

นึกภาพว่าถ้า Unix pipe มันเขียนว่า wc(tar('xf', gzcat(file)))ก็คงใช้ลำบากกว่า gzcat file | tar xf - | wc

แล้วการเขียน helper มาทำให้มันกลายเป็นซ้ายไปขวา ก็ทำได้ยุ่งยากใน Javascript เพราะถ้าเราเขียน chain(a, instance.methodB, c, d) this ใน methodB ก็จะผิดอีก ต้องมา bind ก่อน ซึ่งไม่มีภาษาอื่นทำกัน (แล้วผมก็ยังไม่เคยเห็นว่ามี use case ไหนเลยที่จะใช้มันโดยไม่ bind)

สรุปคือมันทำได้ทุกอย่างแต่ไม่เก่งสักอย่าง แต่ก็ยังต้องใช้มันอยู่เพราะใน Browser มันผูกขาด

Node.js

ผมจำไม่ได้ว่าสมัยที่ Node.js เพิ่งมาผมคิดกับมันไว้ยังไง (สมัยนั้นเขียน PHP กับ Web.py) น่าจะไม่ชอบที่เอา JavaScript ไปรันบน server เพราะมันช้า

แต่ในปัจจุบันปรากฏว่ามันเร็วมาก เร็วกว่า Python เสียอีกเพราะ JavaScript ใน browser มีเงินลงทุนมหาศาลจากบริษัทเว็บต่างๆ

ข้อดีของ Node คือ async io ซึ่งมาแรกๆ หลายคนไม่เชื่อว่าไม่ต้องมี application server คั่นหน้าแล้ว เอา Node รับ load ได้ตรงๆ เลย

แต่ข้อเสียของ async io ใน Node คือ syntax มันไม่ค่อยสวยเท่าไรนัก ไม่ว่าจะเป็น Callback หรือ Promise + Await ก็ตามมันก็ไม่ดูดีเท่า Sequential (a().b().c() vs. await (await (await a()).b()).c())

นอกจากนี้เนื่องจาก standard library ที่มีให้ค่อนข้างจำกัด ทำให้เกิด npm ecosystem ที่บูมมากๆ อะไรก็อยู่บน npm ได้หมดแม้แต่ application อย่าง Chromium ก็โหลดผ่าน npm ได้ ข้อเสียก็คือแต่ละคนก็เลือกใช้ lib คนละตัวแตกต่างกันไป รวมถึงพอแยกกัน maintenance คุณภาพก็แตกต่างกันออกไปตั้งแต่ระดับดีมากๆ อย่าง React ไปจนถึง left-pad ที่คนทำอยู่ดีๆ ก็เอาลง

พอ community มันเปิดกว้าง มันก็กลายเป็นว่า project structure เปิดมาแต่ละอันไม่ซ้ำกันเลย เวลาอ่านก็ต้องไปนั่งไล่ก่อนว่าทางเข้าทางออกอยู่ตรงไหนบ้าง แล้วแต่ละคนก็จะทำท่าไม่เหมือนกันเลยเพราะคนไม่ค่อยใช้ Framework (อาจจะมีใช้ Next/Nuxt.js บ้างแต่ก็ไม่ใช่ทุกคนจะชอบ)

ข้อจำกัดอีกเรื่องของ Node คือมันทำอะไรที่ CPU bound ไม่ได้เลยเพราะมันเป็น single thread แถมยังไม่มีวิธีแตก thread เลยแม้แต่วิธีเดียวเนื่องจากมันมี Global lock เหมือน Python (แต่ Python ยังมี multiprocess ให้ใน standard library) แต่คิดว่าเร็วๆ นี้น่าจะมา

ตอนนี้จุดเดียวที่เรายังเลือก Node.js อยู่คือ Socket.io เนื่องจากภาษาอื่นๆ ในยุคก่อนมันทำ Streaming ไม่ได้ (thread pool จะเต็มเอา) พอมาถึงยุคนี้ที่ใครๆ ก็ทำ async แล้วก็เลยโดดข้ามไป WebSocket เลย จะมีแต่ Socket.io เท่านั้นที่ทำได้ทั้ง Long polling และ WebSocket ได้ในเวลาเดียวกัน และใช้โค้ดเดียวกัน

Go

ช่วงที่เขียน Rust ผมบอกว่าจะไม่เขียน Go เดี๋ยวใจแตก แล้วก็เป็นอย่างนั้นจริงๆ

Go แก้ทุกปัญหาของ Node ที่ผมบ่นไว้ด้านบนได้เกือบหมด

ภาษามันเป็น Static typed ในขณะเดียวกัน map เป็น first class data structure ของภาษา ไม่เหมือนภาษายุคก่อนหน้า และมันยังไม่กลัวที่จะใช้ interface{} type

Go มี Standard library + golang.org/x ที่ค่อนข้างดีทำให้ third party ไม่ต้องนั่งตบตีกันว่า HTTP client ต้องใช้ตัวไหน (Go ก็มีให้เลือก แต่ส่วนมากคนก็ยังเชียร์ http.Client ใน standard library อยู่)

Syntax ของภาษาที่เรียบง่ายมากๆ ทีม Go เคยบอกว่าเขียน Go คือเขียนโค้ด ไม่ใช่ไปนั่งเขียน Type

Go มี gofmt และ compiler เองก็เป็น linter ในตัว (no unused variable, enforce ชื่อ function ตัวใหญ่เป็น public เป็นต้น) ทำให้โค้ด Go ไม่ว่า project ไหนส่วนมากก็แทบจะเป็นแบบเดียว

ตั้งแต่เขียนโค้ดมาคิดว่าเรา contribute Go pull request มากที่สุดแล้วถึงจะมาเขียนหลังสุด เพราะมันไม่ต้องกลัวว่า maintainer จะด่าอะไรเราในโค้ดมั้ย

(แอบชอบสไตล์ modulename.FunctionName() ของ Go ด้วย อ่านแล้วมันเคลียร์เลยทันทีว่า function มาจากไหน และตัวใหญ่ทำให้สะดุดตาโดดข้ามไปอ่านง่าย ต่างกับใน JS ที่คนชอบ destructure เอาแต่ functionName มา มันเพิ่ม cognitive load ต้องจำว่าตัวแปรนี้มาจากไหน)

วิธีแก้ปัญหา async แล้ว syntax ไม่สวยของ Go คือเลิกทำ async ทุกอย่างกลายเป็น sync อะไรที่อยากจะรันข้างหลังค่อยสั่งเปิด goroutine ซึ่งเพิ่มแค่คำว่า go ข้างหน้า (เทียบกับ Python thread นี่เขียนกันหลายบรรทัดเลย)

ที่คนเขียน Go อาจจะไม่รู้คือที่เห็น Go เป็น sync จริงๆ ด้านล่างมันก็เป็น async คล้ายๆ กับที่ Node ทำ แล้วมันใช้ scheduler เป็น abstraction ทำให้มันกลายเป็น sync (เหยด!)

แล้ว Go ไม่มี Global lock เหมือน Python หรือ JavaScript มันทำให้ performance ยิ่งดีขึ้นไปอีก แต่ก็อาจจะต้องระมัดระวัง race condition เองมากขึ้น สำหรับเราเราคิดว่ามันตื่นเต้นและเป็นประสบการณ์ที่อยากเรียนรู้ เพราะภาษาอื่นที่เขียนมี global lock หมดเลย

เวลา Deploy Go ก็ทำได้ง่ายกว่าภาษาอื่นๆ มาก เพราะมัน compile มาได้ binary ตัวเดียวใหญ่ๆ (20-200MB) แล้วคลิกเดียวรันได้เลย

ทุกวันนี้เราเลยพบว่ามาเขียน Go บ่อยขึ้นมาก แทบทุกอย่างที่ไม่ต้องแตะ Database ก็จะไปเขียน Go แม้แต่ scripting บางทียังเลือกใช้ Go เพราะมัน Deploy ได้ง่ายกว่าเยอะ

ข้อเสียของ Go คือ module ซึ่งมันมาช้าไปหน่อย และก็ยังออกแบบมาไม่ค่อยดีอยู่ดี ปัญหาที่เห็นคือระบบ versioning ที่ไม่เคลียร์เท่า Node.js และการใช้ URL import ทำให้เวลา fork ต้องแก้ทั้ง project (แล้วเกิดอยากจะ merge กลับจะทำได้ยาก)

Go ไม่มี generic ทำให้บางอย่างทำได้ยาก โดยเฉพาะการ search in array ที่เป็น operator ใน Python แต่ใน Go ต้องนั่งเขียน loop ทุกครั้ง

Go ไม่มี implements ที่เอาไว้เช็คว่า function/struct นี้ satisfy interface แล้วหรือยัง ถ้าไม่ตรง มันจะไป error ตรง call site แทนที่จะ error ที่ struct (ซึ่งลำดับการเขียนโค้ด เรามักจะเขียน struct ให้เสร็จก่อนแล้วค่อยไป create + pass ไปยังคนที่ใช้)

แล้วด้วยความที่มันเป็น static typed ก็คิดว่ายังไม่อยากเอามันไปทำงานที่ใช้ JSON มากๆ เท่าไร

Java

ยังไม่แน่ใจว่าไม่ชอบ Java หรือไม่ชอบ Spring แต่ที่แน่ๆ คือไม่ชอบที่ Java เพราะพยายาม violate YAGNI ทุกวันนี้ยังตั้งคำถามอยู่ว่าถ้าให้เรา rebuild ecosystem Java ให้ใช้ standard เหมือน Python (แต่ไม่แก้ compiler) จะอยากเขียน Java มั้ย

ปัญหาของ Java คือ type system มัน strict เกินไป เวลาจะเพิ่มนิดๆ หน่อยๆ ทำไม่ได้ เช่น ถ้ามี request object แล้วอยากจะฝากข้อมูลไปบน request object ก็ต้องทำ object อีกตัวมาครอบแล้ว delegate เข้าไปใน request (เทียบกับ JavaScript/Python ที่ใช้ req.data ได้เลย หรือ Go ก็มี embed struct ไปบน struct อีกตัว)

คนเขียน Java ก็เลยจะนิยมว่าทำ interface ไว้ให้ทุกอย่าง “เผื่อแก้” data ต่างๆ บน class อื่น ต้องวิ่งผ่าน getter เท่านั้น (ใน Go เรา mutate field บน http.Request กันสนุกเลย)

สองคือภาษามันเก่าแล้วมันไม่มี first class map เหมือนภาษาใหม่ๆ ทำให้ท่าที่จะเขียนโค้ดง่ายขึ้นกลับกลายเป็นโค้ดยุบยับที่อ่านลำบาก

สามคือภาษามันไม่มี first class function & class เวลาส่ง function และ class เข้าไปมันจะฟีลเหมือนเขียน reflection หน่อยๆ ซึ่งทำให้หลายๆ ท่าลำบากในการเขียน

หลายๆ ฟีเจอร์พวกนี้ถูกแก้ด้วย code generation บนภาษาใหม่ๆ เช่น Kotlin, Groovy หรือ Scala ที่ข้างหลังก็ไป generate class ออกมาอยู่ดี

PHP

ตอนเรียนจบ PHP เคยอยู่ใน resume ทุกวันนี้หลายๆ ครั้งก็ยังวิเคราะห์ PHP design อยู่ว่ามันพลาดไปตรงไหน

เราคิดว่า Design ของ PHP ออกแบบมาน่าสนใจ มันเป็นภาษากระแสหลักภาษาเดียวที่ออกแบบมาให้เขียนเว็บเพียงอย่างเดียว

ไม่มีภาษาอื่นไหนที่เขียน hello world เป็นหน้าเว็บได้ใน 1 บรรทัด อย่างน้อยๆ ก็ต้องเขียนส่วนที่รับ request object เข้ามาก่อน แต่ใน PHP มันอยู่ใน global มาตั้งแต่แรก

แล้วถ้าอยากจะ set header สักตัว ก็แค่เพิ่ม header("Content-Type: application/json"); จากหนึ่งบรรทัดกลายเป็นสองบรรทัด

ตัวภาษาเองยัง compact พอที่จะเป็น template engine ในตัวด้วย อย่างที่ WordPress ทำอยู่

แต่ community PHP อยู่ดีๆ ก็อยากเป็น Java แล้วทิ้งพวกนี้ไปโดยไปทำ Symfony และ Laravel มา

จากภาษาที่ echo ก็ออกหน้าจอได้แล้ว กลายเป็นว่าต้อง return Response object กลับไปเหมือนภาษาอื่นๆ แถมยังพยายามทำเป็น OOP จัดๆ ทั้งๆ ที่ตอน PHP4 ไม่มี class เลย พวก function ต่างๆ เช่น echo หรือ header กลายเป็นสิ่งที่ไม่ควรใช้

ก็ไม่รู้ว่า PHP พลาดไปตรงไหน เพราะ Zend หรือเปล่านะที่พยายามทำให้ PHP กลายเป็นการค้าแล้วก็ทำให้มันขายได้ หรือเพราะช่วงเปลี่ยนผ่านที่คนยังไม่ซื้อ VPS กันแล้วจำเป็นต้องใช้ PHP (แล้วที่ JavaScript ทุกวันนี้มันเป็นแบบนี้ ก็เพราะเหตุผลนี้หรือเปล่านะ?)

ทุกวันนี้ก็ยังฝันอยู่ว่าถ้าสร้าง PHP ขึ้นมาใหม่ในยุคนี้จะออกแบบมันยังไงดี แล้วมีใครทำแล้วหรือยังนะ?

ที่เอา PHP ออกจาก resume ตอนนี้เพราะตั้งแต่มันมี Symfony และ Laravel มา ก็ไม่ได้ตามแล้ว เคยเขียนอยู่ 2 project ใน stack นั้นได้ ก็ยังคิดว่า Django ดีกว่า

สองคือ คนเขียน PHP แย่ๆ มีเยอะในประเทศนี้เลยไม่อยากเสี่ยงไปทำงานกับคนพวกนี้ (เหมือนที่ 37signals ใช้ Ruby เพราะใครเขียนเป็นในยุคนั้นคือเซียน จะได้คัดกรองคนง่าย) สมัยที่เคยทำ freelance มา ทุก project ที่มี PHP code เรา penetration test แล้วเจอช่องโหว่ทุกอัน พอแจ้งไปก็มักจะมีข้ออ้างที่ไม่สนใจจะแก้ (เคยเจอขนาดว่าแก้ให้แล้วมา overwrite ทับ)

สามคือคนที่ยังใช้ PHP อยู่ ส่วนนึงคือติดอยู่ใน environment ที่ต้องใช้ PHP (และมักจะเป็นรุ่นเก่า) เราคิดว่าพอหลุดพ้นจาก environment นั้นไปได้แล้ว มันเปิดกว้างให้ใช้ภาษาอะไรก็ได้

Rust

เคยจับ Rust มาผ่านๆ ด้วยเหตุผลว่าเราไม่มีภาษาไหนเขียน low level เลย (ตอนนั้นยังไม่ได้เขียน Go)

ปัจจุบันก็คงคิดว่าไม่กลับไปเขียนแล้ว เพราะ ecosystem มันพร้อมเลยสำหรับงาน high level และ borrow checker ที่ทำให้เรา unproductive (ถ้ายอมมี garbage collection แล้วเขียนโปรแกรมได้เร็วขึ้น 10 เท่า สำหรับงานที่ผมทำมันไม่เลือกก็พลาดแล้ว)

It Depends

สรุปว่าเราเลือกใช้ตามนี้

  • ถ้าทำเว็บ ต่อ Database ใช้ Python และ Django
  • ถ้าทำอะไรที่ไม่ต้องต่อ Database (เช่น stateless API) อาจจะใช้ Go
  • บน browser ยังคงต้องใช้ JavaScript อยู่
  • Scripting ต่างๆ ถ้าเอาเร็วใช้ Python
  • แต่ถ้าจะเอาความ Deploy ง่าย ใช้ Go

ตอนนี้ยังไม่มีแผนจะเรียนภาษาเพิ่ม เพราะดูจะยังไม่มี use case ไหนขาดตกจนใช้ภาษาที่เขียนแก้ไม่ได้เลยเลยสักภาษา แล้วพอเรียนภาษาใหม่แต่ไม่ได้ใช้มันก็จะลืม (เคยจะลองเขียน Ruby แต่พอพบว่ามันทับซ้อนกับ Python ก็ไม่ได้ใช้ ลืมหมดแล้ว)

สุดท้ายแล้วถึงแต่ละภาษาจะมีจุดแข็ง – จุดอ่อนต่างๆ และมี clear use case ที่เราเลือกใช้ แต่หลายๆ ครั้งก็จะเลือกตามนั้นไม่ได้ถ้าทำหลายคน แล้วคนอื่นไม่ได้เขียนด้วย อันนั้นก็ต้องคุยกันอีกที