--- title: Gleam - một lựa chọn mới cho lập trình web frontend date: 2026-03-02 17:18:08.611413 UTC --- Gần đây mình thử nghiệm phát triển web frontend với ngôn ngữ Gleam, framework Lustre và khá ưng ý. Mình đã áp dụng luôn cho hai dự án cá nhân. Lý do khiến mình "đứng núi này trông núi nọ": - Viết JavaScript hoài mà không mê nó được. JavaScript có nhiều điểm kỳ cục khiến mình không thấy thoải mái. Thật ra thì mình viết TypeScript không đó chứ. Hệ thống kiểu (type system) của TypeScript nói chung là rất tân tiến, mình nể phục người thiết kế ra nó. Tuy nhiên vì nó vẫn phải bám theo JavaScript nên thừa hưởng những thứ kì cục của JavaScript. - Các framework bên frontend sáng tạo mạnh quá nên gây khó khăn trong việc tích hợp TypeScript. Ví dụ, VueJS sáng chế ra ngôn ngữ template của riêng nó, dựa trên HTML, thêm các directive `v-if`, `v-for` chứa code nhúng JavaScript. ```vue ``` Khi dùng VueJS với TypeScript thì người ta có nhu cầu dùng TypeScript để kiểm tra được kiểu cho các đoạn code nhúng này luôn. Từ kiểm tra kiểu, ta còn cần mang nó lên LSP server để cung cấp tính năng auto-complete, soi kiểu khi rê chuột cho code editor. Vấn đề là để tích hợp TypeScript vào những ngôn ngữ "tự chế" như thế không đơn giản. Thư viện Volar, lõi của [công cụ][vue-language-tools] phục vụ cho công việc lập trình với VueJS, phải [ghi đè][hack-node] cả một số hàm built-in của NodeJS. ```js import * as fs from 'fs'; (fs as any).readFileSync = (...args: any[]) => { // code } ``` Nếu theo dõi quá trình phát triển của Volar/ Vue Language Tools ta cũng thấy tác giả phải thử nghiệm nhiều kiến trúc khác nhau để giải quyết vấn đề tích hợp với TypeScript, và cùng lúc phải đánh đổi với việc để nảy sinh vấn đề khác: kém ổn định, chạy chậm, tốn CPU, bộ nhớ. Thậm chí tới phiên bản v3 thì Volar [tự chế][custom-lsp] giao thức LSP khiến không còn tương thích với một số editor nữa. Mình khá thích dùng phần mềm [Helix][helix] để viết code, vì các thao tác chọn, sửa theo khối, nhảy cóc tiện hơn các trình biên tập có giao diện đồ họa (như VS Code) nhiều. Thế nhưng do Volar dùng LSP tùy biến mà Helix thì chỉ đồng ý bám theo giao thức LSP gốc nên mình phải bám víu VS Code. ## Vậy Gleam như thế nào mà không dính vấn đề đó Gleam là một ngôn ngữ có hệ thống kiểu mạnh ngay từ đầu. Ai đã lập trình Rust, cảm thấy lợi ích từ hệ thống kiểu (type system) của Rust sẽ gặp lại cảm giác đó khi làm việc với Gleam. Một điều nhẹ nhõm là Gleam dễ hơn Rust nhiều. Sau đây là một số điểm chính: ### Xử trí lỗi bằng kiểu `Result` Lợi ích là nhìn vào signature của hàm, ta biết trước hàm đó sẽ trả về những lồi nào, và trình biên dịch cũng sẽ buộc ta phải xử trí trường hợp lỗi. Một ví dụ trong tài liệu của Gleam: ```gleam pub type PurchaseError { NotEnoughMoney(required: Int) NotLuckyEnough } fn buy_pastry(money: Int) -> Result(Int, PurchaseError) {} ``` Trước khi dùng, ta cũng có thể thấy được hàm `buy_pastry` nếu thất bại thì sẽ trả về lỗi "không đủ tiền mua bánh" hoặc "tiệm hết bánh". ### Kiểu dữ liệu đại số (algebraic type) Mình không biết dịch từ này thế nào cho hay, đại khái nó là một kiểu dữ liệu mở rộng của enum, cho phép enum đính kèm thêm dữ liệu. JavaScript có một thiếu sót là ngay cả `enum` cũng không có. Kiểu `enum` cho phép ta quy định một biến chỉ được nhận một trong các giá trị hữu hạn nào đó. Ví dụ ta có thể phân quyền người dùng bằng `enum`: "superuser" (có mọi quyền), "staff" (được vào trang admin), "regular", "robot". Cách này an toàn hơn cách dùng các cờ boolean `is_superuser`, `is_staff`, `is_robot` vì nó ngăn ngừa tình huống một tài khoản chỉ là "robot" mà lại có quyền "superuser"! Tuy nhiên, trong lập trình hiện đại, `enum` thôi cũng chưa đủ, nhiều khi ta cần kèm thêm dữ liệu cho một lựa chọn nào đó của enum nữa. Ví dụ với hệ thống phân quyền trên, giả sử ta muốn kèm thêm thông tin là "staff" đó được phân công quản lý phân mục Admin nào, ta có thể định nghĩa như sau: ```rust enum UserRank { SuperUser Staff {section: AdminSection} Regular Robot } ``` Kiểu `enum` như vậy chính là algrebraic type. Trong ví dụ `buy_pastry` phía trên, `PurchaseError` chính là một enum algerabic trong Gleam. ### So khớp đồng dạng (match) Rust và Python có cú pháp `match`, gọi là "structural pattern matching", nó là một dạng tân tiến hơn "switch case" vì nó không so sánh ngang bằng về giá trị mà so sách xem giá trị đồng dạng với cấu trúc nào, đồng thời cho phép bóc tách từng phần của giá trị đó. Ví dụ mình làm một tính năng đồng bộ dữ liệu giữa hai hệ thống khác nhau, có tên là Kaon và Markaz. Mỗi khi dữ liệu trên Kaon thay đổi, nó sẽ phát ra các message, và hệ thống Markaz nhận message này, kiểm tra nó thuộc loại nào, bóc thông tin ra rồi chuyển đổi, lưu lại. Giả sử mình định nghĩa message đó thuộc các kiểu như sau: ```py class KCourseDeleteEventMessage(BaseModel): content_type: Literal[KContentType.COURSE] action: Literal[DataActionType.DELETE] course_id: PositiveInt class KCourseBodyEventMessage(BaseModel): content_type: Literal[KContentType.COURSE] action: Literal[DataActionType.CREATE, DataActionType.UPDATE] data: KCourse class KInstitutionDeleteEventMessage(BaseModel): content_type: Literal[KContentType.INSTITUTION] action: Literal[DataActionType.DELETE] institution_id: PositiveInt class KInstitutionBodyEventMessage(BaseModel): content_type: Literal[KContentType.INSTITUTION] action: Literal[DataActionType.CREATE, DataActionType.UPDATE] data: KInstitution ``` Khi nhận message, Markaz sẽ so khớp như sau: ```py def handle_kaon_message(msg: KDataChangeMessage, entry_id: str) -> None: match msg: case KCourseBodyEventMessage(action=DataActionType.UPDATE, data=kcourse): # Tìm Course đang có, cập nhật dữ liệu liên quan, theo `kcourse` case KCourseBodyEventMessage(action=DataActionType.CREATE, data=kcourse): # Tạo Course mới từ `kcourse`. case KCourseDeleteEventMessage(course_id=course_id): # Xóa Course theo `course_id` ... ``` Gleam cũng có cú pháp này, nhưng đặt tên là `case` và nó luôn luôn được dùng khi lập trình frontend theo framework Lustre. So với `match` của Python thì `match` trong Rust và `case` trong Gleam có ứng dụng rộng rãi hơn, do nó phù hợp nhất với thiết kế "mọi thứ là expression" của Rust & Gleam. Ví dụ mình đang viết đoạn code để xác định trạng thái login hiện tại là gì, để quyết định sẽ hiện giao diện nào, hay đẩy người dùng sang trang Login. Mình sẽ định nghĩa `enum` `LoginState` như sau (để ý nó là algebraic type) ```gleam pub type LoginState { NonLogin TryingLogin(Form(LoginData)) LoggedIn(User) } ``` Trước tiên, mình thử nạp thông tin đã lưu trong storage của trình duyệt, nó bao gồm đối tượng `User` và tình trạng việc xác thực trước đó đã bị hết hạn chưa: ```gleam let #(saved_user, auth_expired) = case store.load_user() ``` Nếu người dùng đang ở trong trang Login rồi thì không càn quan tâm lắm, sẽ hiện form đăng nhập, còn nếu `saved_user` có giá trị và `auth_expired == False` thì là người dùng mới login, sẽ hiện nội dung theo mục đích của trang, còn không thì "đá", ta sẽ dùng `case` để so như sau: ```gleam let login_state = case route, saved_user, auth_expired { LoginPage(_u), _, _ -> TryingLogin(create_login_form()) _, Some(user), False -> LoggedIn(user) _, _, _ -> NonLogin } ``` Rất tiện phải không, chẳng cần tạo hàm mới, chẳng cần `if else` lằng nhằng (mà thực ra thì Gleam cũng chắng có `if else` cơ). ### Công cụ kiểm tra, gợi ý đầy đủ Như nãy mình nói, vấn đề của các framework JavaScript là chúng nhồi nhét đủ thứ ngôn ngữ vào một file, khiến việc tích hợp TypeScript và các công cụ kiểm tra, format khá vất vả. Để "lint" file Vue thì chỉ có ESLint hỗ trợ, nhưng ESLint lại quá chậm, còn những công cụ mới, chạy nhanh nhẹ như Oxlint, Biome thì không hỗ trợ file Vue. Với Gleam thì bạn đã có đầy đủ vì trình biên dịch Gleam có luôn tính năng LSP (autocomplete), format code luôn. Framework Lustre cũng chẳng chế thêm cú pháp mới nào cả, bạn vẫn viết code Gleam bình thường để sinh ra DOM, HTML, ví dụ: ```gleam fn render_flash_message(message: FlashMessage) -> Element(b) { let #(color_class, icon) = case message.severity { Success -> #("bg-emerald-500", icons.circle_check([a.class("w-8")])) Info -> #("bg-blue-500", icons.circle_alert([])) Warning -> #("bg-yellow-400", icons.circle_alert([])) Danger -> #("bg-red-500", icons.flame([])) } h.div( [ a.class("flex items-center justify-between px-6 py-4"), a.class(color_class <> " transition-opacity duration-1200"), ], [ h.div([a.class("flex flex-row items-center")], [ icon, h.p([a.class("ms-3")], [h.text(message.content)]), ]), h.button( [ a.class( "p-1 transition-colors duration-300 transform rounded-md hover:bg-opacity-25 hover:bg-gray-600 focus:outline-none", ), ], [], ), ], ) } ``` Hàm đó sẽ sinh ra HTML như sau: ```html
(Elements for icon and message content)
``` Nhìn hai code cũng khá tương đồng, đúng không. Công cụ đầy đủ, dùng giao thức LSP chuẩn nên với Gleam mình lại được múa may trong trình biên tập Helix yêu thích. ![Lustre view](https://quan-images.b-cdn.net/blogs/2026/03/Screenshot%20From%202026-03-03%2000-29-04.png) Khi biên dịch, Gleam có khả năng tạo ra file _.d.ts_ để bạn có thể import hàm viết bằng Gleam vào code TypeScript, tuy nhiên, tính năng "cộng tác với TypeScript" này lúc chạy được, lúc không (TypeScript không nhận được kiểu), mình cũng chưa biết sai chỗ nào :D ## Những điều chưa hài lòng Chả có gì là hoàn hảo cả, sau đây là những điều mình không đồng ý lắm với tác giả của Gleam: - Xài chung một cú pháp cho cả `struct` và `enum`, ví dụ nếu `type` chỉ có một variant thì là `struct`, có nhiều thì là `enum` (thực ra Gleam gọi chung là "custom type"): ```gleam type LoginData { LoginData(email: String, password: String) } type LoginState { NonLogin TryingLogin(Form(LoginData)) LoggedIn(User) } ``` Hậu quả là có sự trùng lặp code khi định nghĩa `struct` (Gleam gọi là [`record`][record]). Một hậu quả khác là các variant của `enum` sẽ lọt tên ra phạm vi toàn cục, nghĩa là ta không thể định nghĩa hai enum có variant trùng tên: ```gleam type LoadingStatus { Idle IsLoading IsSubmitting } type UserStatus { Idle Chatting } ``` Gleam không chấp nhận vì `Idle` bị trùng, và Gleam cũng không cho viết lối "namespace": `LoadingStatus.Idle`, `UserStatus.Idle`. ## Bạn có nên dùng Gleam Tin buồn là kho thư viện của Gleam ([packages.gleam.run][package-registry]) khá khiêm tốn, nên nếu bạn thích một ngôn ngữ được thiết kế tốt, công cụ hỗ trợ tốt, không ngại tự viết thư viện thì Gleam là một thanh gươm tốt. Còn bạn muốn làm ra sản phẩm nhanh, cần gì thì lấy thư viện về xài, lại muốn có AI để nó viết code thay cho thì... thôi có lẽ "chúng ta không thuộc về nhau". [vue-language-tools]: https://github.com/vuejs/language-tools [hack-node]: https://github.com/volarjs/volar.js/blob/e08f2f449641e1c59686d3454d931a3c29ddd99c/packages/typescript/lib/quickstart/runTsc.ts#L40 [custom-lsp]: https://github.com/vuejs/language-tools/tree/master/packages/language-server#collaboration-with-typescript-plugin [helix]: https://helix-editor.com/ [record]: https://tour.gleam.run/everything/#data-types-records [package-registry]: https://packages.gleam.run/