8 คะแนน โดย GN⁺ 2023-08-20 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ภาษาที่ใช้พัฒนาเครื่องมือสายคอมไพเลอร์นั้นตามธรรมเนียมมักแบ่งเป็น OCaml และ C++ แต่สำหรับการทดลองภาษาเล็ก ๆ TypeScript ก็อาจเป็นตัวเลือกที่ใช้งานได้คล่องแบบภาษาในตระกูล ML
  • Rust ผสานข้อดีของ ML และ C++ พร้อมให้ความสามารถด้านมัลติเธรดที่ปลอดภัย แต่ต้องคอยจำลองการจัดวางข้อมูลในหน่วยความจำ จึงอาจเป็นภาระเกินไปสำหรับ ต้นแบบขนาดเล็ก
  • Deno ช่วยให้เริ่ม การทดลองภาษาด้วย TypeScript ได้รวดเร็ว ด้วยไบนารีเดียว มี linting และ formatting ในตัว ไม่มีขั้นตอนคอมไพล์ และมี task runner กับ watch mode
  • ตัวอย่าง type checker ใช้การผสมกันของ AST แบบ generic, tagged union, visitor และ bottom-up transform เพื่อแปลง Expr<void> เป็น Expr<Type> และใส่ TypeError เป็นค่าชนิดข้อมูลเพื่อลด ข้อผิดพลาดลูกโซ่
  • TypeScript อาจเป็นเครื่องมือที่มีประสิทธิภาพสำหรับการแฮ็กภาษาเล็ก ๆ เพราะมี autocomplete, ระบบชนิดข้อมูลที่ยืดหยุ่น และคุณสมบัติรันไทม์ที่ถอยไปใช้แนวทาง dynamic ได้เมื่อจำเป็น

ภาพเดิมของการเลือกภาษาเพื่อใช้พัฒนา

  • เมื่อสร้างเครื่องมือแนวคอมไพเลอร์ การเลือกภาษาที่ใช้พัฒนามักมีอยู่สองแนวทางใหญ่
    • งานที่เน้นตัวภาษา เช่นสเปกเชิงรูปแบบหรือภาษาของเล่นสำหรับงานอดิเรก มักเหมาะกับ OCaml
    • ตัวอย่างเช่น plzoo และ WebAssembly reference interpreter
  • งานที่เน้นการติดตั้งใช้งานจริงและต้องพร้อมสำหรับโปรดักชัน มักเลือก C++
    • LLVM, clang, v8, HotSpot ล้วนพัฒนาด้วย C++
  • Rust ได้รับอิทธิพลโดยตรงจาก ML และ C++ รวมข้อดีของทั้งสองภาษาไว้ด้วยกัน และยังมีจุดแข็งเฉพาะตัวอย่างมัลติเธรดที่ปลอดภัย
    • อย่างไรก็ตาม บนสเปกตรัมนี้มันเอนมาทาง ความพร้อมระดับโปรดักชัน มากกว่า
    • ระบบ build ที่ “แค่ใช้งานได้เลย” ช่วยการทำต้นแบบได้ก็จริง แต่ก็มาพร้อมความซับซ้อนเพิ่มจากการต้องจำลองการจัดวางข้อมูลจริงในหน่วยความจำ

ข้อดีของแนวทาง index ใน Rust และภาระสำหรับโค้ดขนาดเล็ก

  • คำแนะนำที่พบบ่อยเมื่อสร้างคอมไพเลอร์ด้วย Rust คือหลีกเลี่ยง pointer แล้วใช้ index แทน
  • index มีข้อดีหลายอย่างใน codebase ขนาดใหญ่
    • สามารถเก็บ side table ไว้ในโมดูลที่เกี่ยวข้องได้ ทำให้ coupling ต่ำลง
    • index ที่เป็น u32 และการจัดวางแบบ struct-of-arrays เอื้อต่อประสิทธิภาพ
    • ทำ serialization หรือเชื่อมเข้ากับเฟรมเวิร์ก incremental compilation ได้ง่าย ทำให้กลยุทธ์การคำนวณยืดหยุ่นขึ้น
  • แต่ในการเขียนโปรแกรมขนาดเล็ก แนวทางแบบ index เองก็กลายเป็นเรื่องจุกจิก และสำหรับการทดลองเชิงงานอดิเรก ภาระนี้อาจร้ายแรงถึงขั้นทำให้ไม่อยากทำต่อ
  • OCaml ยังให้ความรู้สึกเก่าอยู่บ้าง และในบริบทนี้จึงมีการพิจารณาว่า TypeScript จะใช้เป็นทางเลือกในบทบาทที่คล้ายกับ ML ได้หรือไม่

Deno และ TypeScript กับสภาพแวดล้อมทดลองที่เริ่มได้เร็ว

  • ใช้ deno เป็นสภาพแวดล้อมเริ่มต้น
    • ให้ประสบการณ์แบบ พร้อมใช้ทันที สำหรับ TypeScript
    • จุดนี้เป็นความลำบากใน OCaml ส่วน Rust ดีกว่า OCaml หรือ C++ แต่ Deno ก็ยังให้ประสบการณ์ที่ง่ายกว่า Rust
  • ประสบการณ์พัฒนาของ Deno เหมาะกับการแฮ็ก PLT ขนาดเล็ก
    • เป็นไบนารีเดียว
    • มี linting และ formatting ในตัว
    • ไม่มีขั้นตอนคอมไพล์แยกต่างหาก
    • มี task runner และ watch mode ในตัว
  • ตัว TypeScript เองก็มี ระบบชนิดข้อมูล ที่ยืดหยุ่นพอ และมีภาระด้านไวยากรณ์ไม่มาก

แพตเทิร์นของ TypeScript จากตัวอย่าง type checker ขนาดเล็ก

  • AST เริ่มต้นจากนิพจน์ที่มีข้อมูลตำแหน่งในไฟล์
    • Location มี file, line, column
    • ใน TypeScript สตริงก็เป็นแค่ string ตัวเลขก็เป็นแค่ number จึงไม่ต้องกังวลกับการแยกแบบ usize กับ u32
  • นิพจน์แยกตำแหน่งกับ kind ออกจากกัน และต่อมาทำให้ข้อมูลที่เกี่ยวข้องเป็น generic ในรูป Expr<T>
    • นิพจน์ทันทีหลัง parse จะมีข้อมูลเป็น void
    • นิพจน์ที่ผ่าน type checker แล้วจะมีข้อมูลเป็น Type
    • ฟังก์ชัน type inference รับ ast.Expr<void> และคืนค่า ast.Expr<Type>
  • TypeScript ไม่ได้เพิ่มพฤติกรรมรันไทม์ให้อัตโนมัติ ดังนั้นหากต้องการ match กับ union type ก็ต้องใส่ข้อมูลชนิดที่รันไทม์เอง
    • ฟิลด์อย่าง tag: "binary", tag: "if" ทำหน้าที่นี้
    • tag: "binary" หมายความว่า ณ รันไทม์ ค่านั้นเป็นสตริง "binary" ได้เท่านั้น
  • boolean literal และ int literal มีรูปแบบแทบเหมือนกัน จึงถูกทำให้เป็นนามธรรมด้วย ExprLiteral<T, V, Tag>
    • ExprBool<T> คือ ExprLiteral<T, boolean, "bool">
    • ExprInt<T> คือ ExprLiteral<T, number, "int">
  • ค่าชนิดข้อมูลแบ่งเป็น TypeBool, TypeInt และมี singleton value ชื่อเดียวกันให้ใช้ด้วย
    • TypeScript ลบชนิดข้อมูลทิ้งทั้งหมด จึงทำให้ชื่อฝั่ง type และชื่อฝั่ง value อยู่คนละ namespace
    • ใช้คุณสมบัตินี้เพื่อกำหนดให้ type และ value ใช้ชื่อเดียวกันได้

Visitor, transform, ชนิดข้อมูลข้อผิดพลาด, desugaring

  • switch ของ TypeScript เป็น statement ไม่ใช่ expression ดังนั้นเพื่อจัดการ kind ของนิพจน์ให้สะดวกจึงนิยาม visitor ขึ้นมา
    • เมธอด bool, int, binary, if จัดการแต่ละ kind ตามลำดับ
    • autocomplete ของ editor ช่วยทั้งการเขียน switch case และการ implement visitor
  • transform<U, V> เป็นฟังก์ชัน traversal แบบทั่วไปที่แปลง Expr<U> เป็น Expr<V>
    • การแปลงทำแบบ bottom-up
    • ตอนเยี่ยมชมโหนดภายใน นิพจน์ย่อยจะถูกแปลงไว้แล้ว จึงทำให้ชนิดของ visitor เป็น Visitor<V, V> ไม่ใช่ Visitor<U, V>
  • หากอาศัยความเป็นภาษาชนิดข้อมูลแบบ dynamic ของ TypeScript ก็สามารถใช้ Object.keys เพื่อสร้าง traversal ที่ทั่วไปกว่านี้ได้
    • ถึงอย่างนั้นก็ยังคงลายเซ็นฟังก์ชันแบบ static ไว้ได้
    • ในตัวอย่างนี้ไม่ได้จำเป็นนัก แต่หากต้องการก็มีช่องให้ถอยไปใช้วิธี dynamic ได้
  • ข้อผิดพลาดด้านชนิดข้อมูลไม่ได้สะสมไว้ในอาร์เรย์เป็น side effect แต่แสดงเป็นชนิดข้อมูล TypeError
    • Type จึงกลายเป็น TypeBool | TypeInt | TypeError
    • TypeError มี tag: "Error", location, message
    • type_equal จะคืนค่า true ถ้าฝั่งใดฝั่งหนึ่งเป็น Error เพื่อป้องกัน ความล้มเหลวต่อเนื่องเป็นลูกโซ่
  • type checker สุดท้ายตรวจทั้งนิพจน์ binary และ if
    • ถ้า operand ซ้ายและขวาของนิพจน์ binary มีชนิดต่างกัน จะคืนข้อผิดพลาด "binary expression operands have different types"
    • ถ้าเงื่อนไขของนิพจน์ if ไม่ใช่ boolean จะคืนข้อผิดพลาด "if condition is not a boolean"
    • ถ้า then branch กับ else branch มีชนิดต่างกัน จะคืนข้อผิดพลาด "if branches have different types"
  • ผลลัพธ์สุดท้ายยังต้องเขียน type อยู่พอสมควร แต่ autocomplete ช่วยได้มาก ความรู้สึกเหมือนต้องฝืนสู้กับภาษามีน้อย และรูปแบบโดยรวมก็เข้ากับปัญหาอย่างเป็นธรรมชาติ
  • เหตุผลที่สรุปว่า TypeScript มีประสิทธิภาพสำหรับเครื่องมือแฮ็กภาษาเล็ก ๆ มีอยู่สามข้อ
    • Deno เป็น สคริปต์รันไทม์ ที่เล็ก ปิดจบในตัว ทรงพลัง และเหมาะกับเวิร์กโฟลว์การพัฒนาที่มีประสิทธิภาพ
    • เครื่องมือของ TypeScript ใช้งานกับ IDE ได้ดี มีประโยชน์และช่วยเพิ่มประสิทธิภาพ อีกทั้งด้วย Deno จึงแทบไม่ต้องตั้งค่า
    • ตัวภาษาแข็งแรงทั้งฝั่งรันไทม์และคอมไพล์ไทม์ สามารถแสดงโครงสร้างที่ซับซ้อนพอสมควรผ่าน type ได้ และยังถอยไปใช้แนวทาง dynamic ได้เมื่อจำเป็น
  • อีกแนวคิดหนึ่งที่เสนอไว้คือการ desugar syntax sugar จำนวนมากแบบ type-safe
    • ทำให้ Expr และ ExprKind ไม่ได้พารามิเตอร์แบบข้อมูลที่เกี่ยวข้อง แต่พารามิเตอร์วนกลับไปที่ ExprKind ทั้งก้อน
    • ExprKindCore แทนชุดนิพจน์พื้นฐาน
    • ExprKindSugar รวมทั้งนิพจน์พื้นฐานและนิพจน์ที่ desugar กลับเป็นนิพจน์พื้นฐานได้
    • desugar(expr: ExprSugar): ExprCore จะย่อนิพจน์ syntax sugar ให้เป็นนิพจน์แกนหลัก
    • desugar_one(expr: ExprKindSugar<ExprCore>): ExprKindCore<ExprCore> จะทำการแปลงหนึ่งขั้น เมื่อ subexpression ถูก desugar มาแล้วทั้งหมด

1 ความคิดเห็น

 
GN⁺ 2023-08-20
ความเห็นจาก Hacker News
  • TypeScript โดยรวมเป็นภาษาที่ยอดเยี่ยม และจุดที่ฟังก์ชันเป็นอ็อบเจ็กต์ที่มีพร็อพเพอร์ตี/เมธอดได้นั้นถูกประเมินค่าต่ำไป
    สามารถรันอาร์เรย์ของฟังก์ชันเหมือนเป็นคำสั่ง แล้วค่อยเพิ่มคำอธิบายอย่าง help ในภายหลัง หรือเพิ่มสถานะด้วย closure/partial application ได้ จึงไม่จำเป็นต้องรีบกำหนดคลาสอย่าง Command ตั้งแต่ต้น
    ใน OOP การตั้งชื่อให้เร็วเกินไปมักสร้างความขัดแย้งมากมาย และมองว่าการส่งค่าที่จำเป็นเข้าไปในฟังก์ชันนั้นเป็นธรรมชาติกว่าโครงสร้างแบบ VideoCompressor#compress()

    • พูดให้ถูกคือเป็นความสามารถของ JavaScript มากกว่า TypeScript
      ภาษาอื่น ๆ ที่รองรับอ็อบเจ็กต์ที่ทำงานเหมือนฟังก์ชันถูกรวบรวมไว้ที่ https://en.wikipedia.org/wiki/Function_object
    • ใน Go สามารถผูกเมธอดกับฟังก์ชันได้ และจริง ๆ แล้วทำได้กับแทบทุก type
      เช่น handler ใน net/http โครงสร้างอาจ implement เมธอด serve ก็ได้ หรือ handler function อาจเรียกตัวเองเพื่อทำให้ตรงตาม interface ก็ได้
      ใน Clojure สามารถแนบ metadata กับ var ของฟังก์ชันเพื่อทำคล้าย ๆ กันได้ และเพราะเป็น Lisp จึงทำแทบอะไรก็ได้ด้วย macro
      อีกทั้ง CSP channel ของ core/async ยังแยกการประมวลผลกับการสื่อสารออกจากกัน ช่วยเลี่ยงปัญหาเรื่องสีของฟังก์ชันอย่าง callback/promise/async/await และทำให้คำสั่งต่าง ๆ ทำงานเหมือน producer ที่ส่งผลลัพธ์ไปยัง channel ได้
    • ใช้ single-method interface ของ Java ก็ให้ผลคล้ายกันได้
      ไม่ว่าเมธอดจะชื่ออะไร ก็สามารถใช้ในบริบทของฟังก์ชันได้ และไม่จำเป็นต้องอ้างถึงชื่อเมธอดเฉพาะ
      อย่างไรก็ตาม ไม่ค่อยชอบกรณีที่ฟังก์ชันมีพร็อพเพอร์ตีเพื่อเก็บสถานะไว้ และอาจเปลี่ยนพฤติกรรมแม้เรียกด้วยอาร์กิวเมนต์ชุดเดิม มองว่าข้อดีสำคัญของ functional programming คือการหลุดพ้นจาก state แบบ OOP
    • Python รองรับแนวทางนี้ได้ดี เพราะทุกอย่างเป็นอ็อบเจ็กต์
    • ใน C# ก็สามารถแสดงแบบคล้ายกันได้ด้วย Func สำหรับฟังก์ชันที่มีค่าคืนกลับ และ Action สำหรับฟังก์ชันที่ไม่มีค่าคืนกลับ
      lambda expression ของ JavaScript ค่อนข้างคล้ายกับ C# และ signature ของฟังก์ชันใน TypeScript กับ C# ก็คล้ายกันพอสมควร
      มี repository เล็ก ๆ ที่แสดงความคล้ายกันของ JavaScript, TypeScript และ C#: https://github.com/CharlieDigital/js-ts-csharp
      สกรีนช็อตที่แสดง logic เดียวกันเป็น JS/TS/C# เคียงกัน: https://github.com/CharlieDigital/js-ts-csharp/blob/main/js-...
  • ก็ไม่ได้แปลกใจมากนัก มองว่า TypeScript สุดท้ายแล้วก็เป็นอีกภาษาหนึ่งที่ค่อย ๆ รับความสามารถจำนวนมากจากตระกูล ML เข้ามาอย่างยากลำบาก
    เพราะไม่มี pattern matching แท้ ๆ จึงไม่สะดวกเท่า OCaml แต่ถ้าเทียบกับกลุ่มอย่าง C#, Swift, Dart, Kotlin ก็ถือว่าอยู่ในระดับใช้ได้

    • เป็นการเปรียบเทียบที่มองในระดับสูงเกินไป ประสบการณ์ใช้งานจริงต่างกันพอสมควร
      TypeScript มีระบบ type ที่ทรงพลัง แต่ standard library พื้นฐานและตัวภาษายังน่าเสียดาย และไม่มี pattern matching/switch expression
      Dart มี object model แบบปิด ทำให้เสรีภาพเชิง dynamic ต่ำกว่า ระบบ type และ expression ก็อ่อนกว่า อีกทั้งแทบไม่มี facility ด้าน metaprogramming จึงต้องพึ่ง boilerplate แบบ Java และ code generator
      C# เป็นภาษาที่ใกล้กับความสามารถแบบ ML มากที่สุดในบรรดาภาษาที่กล่าวถึง แต่ต่างจาก TypeScript ตรงที่ไม่มี sum type ทำให้หลายอย่างยุ่งยากกว่า
    • ก่อนหน้านี้ในคอมไพเลอร์สำหรับการเรียนรู้แนว Pascal-C ที่ทำด้วย Haskell, parser combinator ทำให้สามารถแสดงไวยากรณ์โดยตรงในโค้ดคล้าย BNF ได้
      เช่น นำ parser ที่รู้จัก { ... } ไปประกอบกับ parser อื่น ๆ และนิยาม statement ให้เป็นอย่างใดอย่างหนึ่งระหว่าง control flow/declaration/assignment ได้
      การประมวลผล list แบบ ML และ pattern matching แสดงพลังได้ดีมากเมื่อจัดการ intermediate representation
    • เวลาต้องใช้ pattern matching ใน TypeScript มักใช้ไลบรารีนี้: https://github.com/gvergnaud/ts-pattern
    • คงพูดได้ยากว่า TypeScript นำความสามารถส่วนใหญ่ของ ML มาใช้แล้ว
      แม้ในบทความก็ต้องอ้อมไปใช้ visitor pattern เพราะ switch ไม่ใช่ expression และการรองรับ iterator ของ JavaScript ก็ขาด ๆ เกิน ๆ อย่างประหลาด
      มี .map() แต่ใช้ได้เฉพาะกับ array และไม่สามารถนำไปใช้กับ iterator ทั่วไปได้โดยตรง
    • ถ้าใช้ TypeScript กับ kotlin-js จะรู้สึกว่าทั้งสองภาษาค่อนข้างใกล้กัน
      แม้มีความต่างมาก แต่การสลับไปมาไม่ยาก และโดยส่วนตัวแม้จะชอบ Kotlin มากกว่า ก็ยังใช้ได้ทั้งคู่
      ชวนคิดว่า ถ้า TypeScript หลุดจากความเข้ากันได้กับ JavaScript แล้วคอมไพล์ไปเป็น WASM จะเป็นอย่างไร Kotlin กำลังเพิ่ม WASM compiler และมี JS transpiler อยู่แล้ว โดยโค้ดเดียวกันโหลดบน WASM ได้เล็กกว่าและเร็วกว่า
      JavaScript บนเบราว์เซอร์ไม่ใช่ target สำหรับการคอมไพล์ที่ดี และเมื่อโปรเจกต์ใหม่ ๆ เริ่มด้วย TypeScript ตั้งแต่ต้นมากขึ้น เหตุผลที่ว่าต้องย้ายจาก JavaScript เดิมได้ง่าย ๆ ก็อ่อนลงเรื่อย ๆ
  • ถ้าสลับใช้ Rust กับ TypeScript จะเห็นชัดมากว่าขาดความสามารถอย่าง tagged enum เพียงใด
    ข้อเสนอ ADT enum ดูเหมือนจะหยุดไปแล้ว เลยสงสัยว่ามีความพยายามอื่นอยู่หรือไม่: https://github.com/Jack-Works/proposal-enum/discussions/19

    • สำหรับการใช้งานคล้าย ๆ กัน มองว่า discriminated union ทำงานได้ค่อนข้างดี
  • ระบบ type ของ TypeScript นั้นน่าสนุก แต่ก็อดสงสัยไม่ได้ว่าถ้าคอมไพเลอร์เขียนด้วยภาษาที่คอมไพล์แล้วจะเร็วขึ้นอีกแค่ไหน
    แน่นอนว่าต้องมีสมมติฐานใหญ่คือ “การ implement ที่ดี”

    • swc และ esbuild ไม่ใช่ตัวเปรียบเทียบที่ดีนัก เพราะความเร็วที่เพิ่มขึ้นส่วนใหญ่เกิดจากการตัดไวยากรณ์เฉพาะของ TypeScript ออกเพื่อสร้าง JavaScript
      tsc มักจะช้าเฉพาะตอนรันครั้งแรก และถ้าใช้แฟล็ก incremental หรือโหมด watch แบบ --transpile-only เวลาคอมไพล์โดยทั่วไปจะลดลงต่ำกว่า 100ms ทำให้แทบไม่รู้สึกต่างจาก SWC หรือ ESBuild
    • มีคำตอบจากทีม TypeScript: https://twitter.com/drosenwasser/status/1260723846534979584
      ประเด็นคือ ถ้าการตรวจ type ของโปรแกรมขนาดกลางใช้เวลา 20 วินาที โดยมากไม่ได้เป็นเพราะมันเป็น JS แต่เป็นเพราะ type เกิด combinatorial explosion
      runtime อื่นอาจให้ประโยชน์ด้าน parallelism หรือเวลาเริ่มต้นได้ แต่เขาบอกว่ายังไม่เคยเห็น benchmark ที่เน้น CPU ซึ่งรองรับการเพิ่มความเร็วโดยรวม 20 เท่า
    • ระบบ type สนุกดี จนกว่าจะนำไปใช้กับ generics แบบสุดทาง
      แล้วพอใช้เวลาทั้งวัน debug logic ของ type แค่ 5 บรรทัด ก็จะเริ่มถามตัวเองว่ามาถึงจุดนี้ได้ยังไง
    • ไม่ต้องจินตนาการอย่างเดียวอีกต่อไปแล้ว: https://github.com/dudykr/stc
      เป็นโปรเจกต์ที่เขียนด้วย Rust โดยนักพัฒนาหลักของ SWC โดย SWC คอมไพล์ TS เป็น JS ส่วน STC ตรวจ type ของ TS
    • ช่วงหลังประสิทธิภาพของคอมไพเลอร์ TypeScript ดีขึ้นมาก
      ว่ากันว่า isolated declaration mode ที่กำลังจะมาในอนาคตสามารถลดเวลาคอมไพล์ได้สูงสุดถึง 75%: https://github.com/microsoft/TypeScript/pull/53463#issuecomm...
  • สำหรับคนที่เพิ่งเริ่มเรียนคอมไพเลอร์ ขอแนะนำหนังสือเล่มนี้: https://keleshev.com/compiling-to-assembly-from-scratch/
    ผู้เขียนอธิบายว่าเขาสร้างคอมไพเลอร์เป็น ARM assembly 32 บิตด้วย subset ของ TypeScript และมันดูแทบเหมือน pseudocode จึงเข้าถึงง่าย

    • ขอแนะนำ Crafting Interpreters อย่างยิ่งด้วย: https://craftinginterpreters.com/
      หนังสือแบ่งเป็นสองส่วน ส่วนแรกสร้าง interpreter ของภาษาโดยใช้ Java และส่วนที่สองคอมไพล์ภาษาเดียวกันเป็น bytecode แล้ว implement virtual machine สำหรับ bytecode ด้วย C
      โค้ดทุกบรรทัดของ implementation ถูกอ้างอิงไว้ในหนังสือ
  • หากต้องการหลีกเลี่ยง visitor pattern สามารถใช้ IIFE style switch ผ่านฟังก์ชัน utility run ได้
    แค่ใช้ switch ภายในฟังก์ชันที่เรียกใช้ทันที แล้วให้มัน infer return type

    • เคยเขียนบทความเกี่ยวกับ pattern นี้ไว้: https://maxgreenwald.me/blog/do-more-with-run
    • จริง ๆ แล้วไม่จำเป็นถึงขนาดนั้น แค่เรียกฟังก์ชันทันที return type ก็ถูก infer แล้ว
      ถ้าไม่ชอบ () ท้ายสุดจนอยากเลี่ยง IIFE ก็แยกนิยามฟังก์ชันไว้ต่างหากแล้วค่อยเรียกก็ได้
  • กำลังเขียนคอมไพเลอร์ด้วย TypeScript อยู่ และเห็นด้วยว่ามันไม่ได้แย่อย่างที่คิด
    ตอนแรกเริ่มด้วย Deno เหมือนผู้เขียน แต่สุดท้ายย้ายไป Bun แม้จะยังมีส่วนที่หยาบ ๆ อยู่บ้าง แต่พอใจกว่า Deno และเร็วมาก
    ในฐานะ frontend ของ parser generator มาตรฐาน Ohm-js ใช้งานค่อนข้างสบาย: https://ohmjs.org/
    คอมไพเลอร์ tsc ทางการใหญ่เกินไปมาก จึงไม่แนะนำให้อ่าน และถ้าอยากดูว่า tsc ทำงานอย่างไร mini-typescript เหมาะกว่า: https://github.com/sandersn/mini-typescript/
    โดยเฉพาะ branch centi-typescript มีประโยชน์: https://github.com/sandersn/mini-typescript/tree/centi-types...
    หวังว่า WASM จะสามารถเข้าถึง GC และ DOM ได้

    • อยากรู้ว่าอะไรทำให้ย้ายไป Bun
  • นึกว่า TypeScript จะมี overhead เพิ่มเพราะ interface แต่ผิดคาด
    เริ่มสงสัยว่าจะนำไปใช้กับด้านอื่นได้ไหม เช่น การ parse ภาษา จะพอใช้ได้หรือเปล่า

    • ใน runtime overhead ของ interface เป็น 0
      มันหายไปทั้งหมดในกระบวนการคอมไพล์
  • ผลลัพธ์ไม่ได้เหนือความคาดหมายมากนัก เพราะ ตัวคอมไพเลอร์ TypeScript เองก็เขียนด้วย TypeScript
    TypeScript ในฐานะ ML ได้รับการพิสูจน์แล้วทุกวันใน production environment หนัก ๆ

  • เคยลองเขียนคอมไพเลอร์ด้วย C# แล้ว สิ่งที่ดูพิเศษในที่นี้น่าจะมีแค่ union type
    โดยส่วนตัวเลือกเลี่ยงความเยิ่นเย้อของ visitor pattern และกำลังรอฟีเจอร์ enum แบบปิดเพื่อให้ตรวจสอบความครบถ้วนได้ตอนคอมไพล์

    • ถ้าใช้ภาษาที่ไม่มี union type หรือ enum แบบ Rust จะคิดถึงฟีเจอร์นั้นมาก
      ทางเลือกทั่วไปมักจะฝืน ๆ เช่น ใช้คลาสหนึ่งแทน sum type โดยมี nullable property N ตัว แล้วพึ่งเงื่อนไขในเอกสารว่า “มีได้เพียงตัวเดียวที่ non-null เสมอ” หรือให้หลายคลาสสืบทอดจากคลาสร่วม ทั้งสองแบบให้ความรู้สึกหนัก
      ถ้าจะสร้าง union type หลายชุดที่มีส่วนทับซ้อนกัน ทั้งสองแนวทางก็ต้องอาศัยการทำซ้ำหรือการจัดองค์ประกอบที่ฉลาด