Tập 31: Result, Option, match — Giá trị của sự thất bại

Một ngôn ngữ lập trình hệ thống chỉ có giá trị khi nó biết từ chối. Khi compiler học cách nói "không" một cách dứt khoát, ranh giới giữa ý định mơ hồ và semantics chính xác bắt đầu được thiết lập.

Chương trước kết thúc khi DUY vừa có binary đầu tiên qua cầu tạm C backend. Đó là một cột mốc vui vẻ, nhưng cũng là một cột mốc dễ tạo ra ảo tưởng. Một compiler biên dịch thành công một ví dụ hello-world đơn giản không có nghĩa là nó đã là một ngôn ngữ hệ thống an toàn. Nó mới chỉ chứng minh rằng luồng đi xuôi (happy path) hoạt động.

Thách thức thực sự của một ngôn ngữ lập trình không nằm ở việc nó cho phép viết cái gì. Nó nằm ở việc nó ngăn chặn cái gì. Ở tầng hệ thống, khi không có garbage collector gánh đỡ, việc compiler thả lỏng cho một chương trình "tạm chạy được" đi qua cũng giống như việc kỹ sư xây dựng đồng ý ký nghiệm thu một cây cầu chỉ vì thấy hôm nay gió lặng.

Chương này kể về giai đoạn DUY bắt đầu mọc răng và học cách nói "không". Từ việc biến thất bại thành một phần của hệ kiểu với ResultOption, cho đến việc dựng lên hệ thống memory model sơ khai với owned, view, region, và bắt các chi phí ẩn phải lộ diện qua effect system. Mỗi bước đi đều là một cuộc chiến chống lại sự mơ hồ, để biến các luật thiết kế thành các gate kiểm tra không thể thương lượng.



Nhiều ngôn ngữ lập trình coi lỗi là một vị khách không mời mà họ buộc phải xử lý bằng cách ném ra một exception hoặc trả về một giá trị null. Nhưng ở tầng hệ thống freestanding, cả hai giải pháp ấy đều mang nợ kỹ thuật nặng nề. Exception giấu luồng điều khiển dưới lòng đất, phá vỡ tính deterministic của chương trình vì đòi hỏi một cơ chế runtime phức tạp để thực hiện stack unwinding (gỡ stack) — điều cực kỳ xa xỉ hoặc bất khả thi trên các hệ thống nhúng không có OS. Null thì đẩy trách nhiệm kiểm tra cho trí nhớ của lập trình viên — một cơ chế lưu trữ có tỉ lệ lỗi khá cao vào lúc hai giờ sáng.

DUY chọn con đường khác: thất bại là một phần của kiểu dữ liệu.

enum Result[T, E] {
  Ok(T),
  Err(E)
}

Để hiện thực hóa điều này, Sprint 09 và 10 buộc tôi phải đối mặt với bài toán kiểu dữ liệu sum types (tagged unions) và mệnh đề match. Nhìn bề ngoài, match trông giống như một câu lệnh switch bóng bẩy. Nhưng dưới gầm bệ của compiler, sự khác biệt giữa chúng là khoảng cách giữa hai triết lý. switch là một cú nhảy không bảo đảm, nơi lập trình viên dễ dàng quên lệnh break hoặc bỏ sót trường hợp; còn match là một hợp đồng bắt buộc phải vét hết mọi khả năng (exhaustiveness checking).

Tôi nhớ đêm trắng ở Sprint 09, khi tôi cố gắng parse một cấu trúc match lồng nhau. Đầu tôi đau nhức khi nghĩ về thuật toán kiểm tra tính toàn vẹn (exhaustiveness). Nếu anh định nghĩa một enum có ba trường hợp, và mệnh đề match của anh chỉ xử lý hai, compiler DUY phải từ chối biên dịch. Sự an toàn không đến từ việc lập trình viên thông minh, nó đến từ việc compiler không cho phép sự cẩu thả đi qua cửa.

Thách thức kỹ thuật lớn nhất ở V0 không phải là cú pháp của match, mà là thuật toán kiểm tra tính toàn vẹn. Khi một enum có chứa payload, hoặc khi người dùng lồng các cấu trúc vào nhau, việc chứng minh compile-time rằng toàn bộ không gian pattern đã được phủ kín mà không có nhánh nào bị trùng lặp là một bài toán phân tích AST phức tạp. Ở những sprint đầu tiên, tôi chấp nhận giới hạn phạm vi: checker chỉ phủ các boolean và integer literals, cùng cấu trúc phẳng của enum. Nhưng quy tắc vàng đã được dựng lên:

Không có wildcard fallback mặc định trừ khi người dùng chủ động viết _.

Nếu anh lười nghĩ về trường hợp lỗi, compiler sẽ bắt anh phải nghĩ. Trải nghiệm viết code lúc đầu có vẻ bực mình hơn, nhưng đó là sự bực mình lành mạnh của một hệ thống từ chối tích lũy nợ kỹ thuật. Khi tôi viết chương trình tự test đầu tiên bằng match và cố tình bỏ quên nhánh Err, nhìn thấy compiler phun ra lỗi đỏ rực từ chối sinh mã, tôi mỉm cười. Đứa con tinh thần của tôi đã bắt đầu biết tự vệ.


Bình luận (0)

Đang tải bình luận...