Làm quen với Racket: Ngôn ngữ lập trình nơi mã nguồn tự viết ra mã nguồn

Công nghệ22 tháng 8, 2026·9 phút đọc

Racket, hậu duệ của Lisp, không chỉ là một ngôn ngữ lập trình mà còn là một nhà máy tạo ra ngôn ngữ. Bài viết này giới thiệu về lịch sử, triết lý homoiconicity và những tính năng độc đáo giúp bạn tự xây dựng cú pháp của riêng mình. Từ khái niệm danh sách cho đến macro, bạn sẽ thấy lý do vì sao Eric S. Raymond gọi việc học Lisp là một trải nghiệm khai sáng.

Làm quen với Racket: Ngôn ngữ lập trình nơi mã nguồn tự viết ra mã nguồn

Làm quen với Racket: Ngôn ngữ lập trình nơi mã nguồn tự viết ra mã nguồn

"Học Lisp đáng giá vì trải nghiệm giác ngộ sâu sắc bạn sẽ có khi cuối cùng hiểu được nó." — Eric S. Raymond

Chào mừng bạn đến với thế giới của một trong những gia đình ngôn ngữ lập trình lâu đời và khác biệt nhất. Nơi mà mã nguồn chính là dữ liệu, dấu ngoặc đơn là cấu trúc thuần túy, và chương trình có thể tự viết ra chương trình khác. Kết thúc bài viết này, bạn sẽ tự tay tạo ra cú pháp của riêng mình.

Một chút lịch sử

Lisp ra đời năm 1958, do John McCarthy phát minh tại MIT. Để bạn hình dung: đây là ngôn ngữ cấp cao lâu đời thứ hai vẫn còn được sử dụng (chỉ sau Fortran từ năm 1957). Python xuất hiện năm 1991, JavaScript năm 1995 — Lisp đi trước chúng hơn 30 năm. Nhiều ý tưởng mà chúng ta coi là "hiện đại" ngày nay thực chất sinh ra từ Lisp:

  • Garbage collection — được phát minh cho Lisp.
  • First-class functions — truyền hàm như tham số, giờ là tiêu chuẩn ở mọi nơi.
  • REPL — vòng lặp đọc-in-thực thi tương tác mà Python, Node, Julia đều có, khởi nguồn từ Lisp.
  • Conditionals as expressions — câu lệnh if trả về giá trị.
  • Homoiconicity — mã nguồn là cấu trúc dữ liệu của chính ngôn ngữ. Đây là điểm quan trọng nhất, chúng ta sẽ quay lại ở cuối.

Trong nhiều thập kỷ, Lisp là ngôn ngữ của trí tuệ nhân tạo. Vào thập niên 70-80, có cả những chiếc máy tính vật lý được thiết kế để chạy Lisp trực tiếp: dòng máy Lisp Machines do Symbolics và LMI sản xuất. Rồi "mùa đông AI" ập đến, nguồn tài trợ cạn kiệt, và Lisp từ ngôi sao rơi xuống thành ngôn ngữ của giới sùng đạo.

Nhưng những ý tưởng thú vị không bao giờ chết — chúng chỉ đột biến mà thôi.

Từ Lisp đến Scheme rồi Racket

Năm 1975, Gerald Sussman và Guy Steele tạo ra Scheme: một Lisp tối giản, thanh lịch, gần như toán học thuần túy. Scheme trở thành ngôn ngữ yêu thích của giới học thuật để dạy lập trình (cuốn sách huyền thoại SICP — Structure and Interpretation of Computer Programs — được viết bằng Scheme).

Năm 1995, nhóm của Matthias Felleisen tạo ra PLT Scheme, một Scheme thiết kế cho giáo dục và nghiên cứu ngôn ngữ lập trình. Đến năm 2010, nó được đổi tên thành Racket, và ngày nay Racket không chỉ là một Scheme: nó là một ngôn ngữ để tạo ra các ngôn ngữ. Phương châm không chính thức của nó là language-oriented programming: nếu vấn đề của bạn cần một ngôn ngữ riêng, Racket cho phép bạn xây dựng nó trong một buổi chiều.

NămSự kiện
1958McCarthy phát minh Lisp tại MIT
1975Sussman và Steele tạo ra Scheme
1984Common Lisp được chuẩn hóa
1995PLT Scheme (tiền thân Racket) ra đời
2007Clojure ra đời (Lisp trên JVM)
2010PLT Scheme đổi tên thành Racket
Hôm nayBạn, đang đọc bài này, sắp viết dấu ngoặc UwU

Ai đang dùng Lisp ngày nay?

Nhiều hơn bạn tưởng:

  • Clojure chạy trong sản xuất tại các ngân hàng, hãng hàng không và startup (Nubank, ngân hàng số lớn nhất Mỹ Latinh, chạy hoàn toàn trên Clojure).
  • Common Lisp (với trình biên dịch SBCL) vẫn sống khỏe trong các hệ thống chuyên gia, lập kế hoạch chuyến bay (ITA Software, được Google mua lại, từng cung cấp nền tảng cho Google Flights), và tính toán khoa học.
  • Emacs Lisp — hàng triệu người chạy Lisp mỗi ngày mà không hề hay biết, vì trình soạn thảo của họ là một trình thông dịch Lisp.
  • Guile/Guix — một bản phân phối Linux hoàn toàn được cấu hình bằng Scheme.
  • Racket có hội nghị thường niên riêng (RacketCon), cộng đồng học thuật và nghệ thuật năng động, được dùng trong nghiên cứu ngôn ngữ, xác minh hình thức (Rosette), xuất bản và typography (Pollen), và giáo dục trên toàn thế giới.

Các Lisp mới vẫn tiếp tục xuất hiện: Fennel (Lisp biên dịch sang Lua, phổ biến trong phát triển game), Janet, Hy (Lisp chạy trên Python)...

Easter egg cho fan TADC:

Trong The Amazing Digital Circus (tập 8, "hjsakldfhl"), khi Kinger mở terminal để thử reset Caine, bạn có thể thấy Caine (một AI sáng tạo được xây dựng từ 1996) được lập trình bằng Lisp. Tập tin có tên đúng nghĩa là Caine-core.lisp.

Một hình ảnh kỳ quặc về chú hề kỹ thuật sốMột hình ảnh kỳ quặc về chú hề kỹ thuật số

Cài đặt (5 phút)

  • Truy cập https://racket-lang.org
  • Tải trình cài đặt cho hệ điều hành của bạn (Linux, macOS, Windows).
  • Mở DrRacket, môi trường phát triển đi kèm.

DrRacket có hai vùng: phía trên để viết định nghĩa (chương trình của bạn), phía dưới là REPL để thử nghiệm trực tiếp. Trên dòng đầu tiên của vùng định nghĩa, gõ:

#lang racket

Dòng đó báo cho Racket biết bạn đang dùng ngôn ngữ nào (nhớ rằng: Racket là nhà máy sản xuất ngôn ngữ, nên bạn phải chọn một).

Nếu thích terminal: lệnh racket cho bạn một REPL, còn raco là trình quản lý gói và công cụ dòng lệnh.

Lần chạm đầu tiên: mọi thứ đều là biểu thức

Trong REPL, thử:

> (+ 1 2)
3
> (* 3 (+ 2 2))
12
> (string-append "hello " "world")
"hello world"

Quy tắc của Lisp nằm gọn trong một dòng:

Mọi thứ đều là (toán tử tham_số1 tham_số2 ...). Luôn luôn. Không ngoại lệ.

Không có thứ tự ưu tiên toán tử để học thuộc, không có cú pháp đặc biệt nào. (+ 1 2) để cộng. (if ...) để rẽ nhánh. (define ...) để đặt tên. Chính những dấu ngoặc trông có vẻ đáng sợ ban đầu lại là sự vắng mặt hoàn toàn của các quy tắc tùy tiện. Sau một tuần, bạn sẽ không còn thấy chúng nữa.

Định nghĩa và hàm

#lang racket

(define pi-approx 3.14159)

(define (circle-area r)
  (* pi-approx r r))

(circle-area 2)   ; => 12.56636
  • define kèm tên tạo hằng số.
  • define kèm (name arguments...) tạo hàm.
  • Chú thích bắt đầu bằng ;.

Hàm ẩn danh dùng lambda (đúng vậy, cái lambda trong lambda calculus của Church từ thập niên 1930 chính là ông tổ lý thuyết của tất cả những thứ này):

(lambda (x) (* x x))          ; một hàm không tên
((lambda (x) (* x x)) 5)      ; => 25, áp dụng trực tiếp

Danh sách: trái tim của Lisp

Lisp viết tắt của LISt Processing (xử lý danh sách). Danh sách là cấu trúc nền tảng:

(list 1 2 3)          ; => '(1 2 3)
'(1 2 3)              ; cách viết tắt, "quote"
(first '(1 2 3))      ; => 1
(rest '(1 2 3))       ; => '(2 3)
(cons 0 '(1 2 3))     ; => '(0 1 2 3)
(length '(a b c))     ; => 3

Chú ý dấu nháy đơn '. Nó bảo Racket: đừng đánh giá cái này, đây là dữ liệu. Hãy giữ kỹ chi tiết này — nó chính là cánh cửa dẫn đến màn ảo thuật cuối cùng.

Hàm bậc cao

Đây là nơi Racket tỏa sáng. Truyền hàm cho hàm khác là điều tự nhiên nhất trên đời:

(map (lambda (x) (* x x)) '(1 2 3 4 5))
; => '(1 4 9 16 25)

(filter even? '(1 2 3 4 5 6))
; => '(2 4 6)

(foldl + 0 '(1 2 3 4 5))
; => 15

map biến đổi, filter chọn lọc, foldl tích lũy. Chỉ với ba hàm này, bạn có thể giải quyết hầu hết các bài toán danh sách mà không cần viết một vòng lặp for nào.

Đệ quy: tư duy theo hình xoắn ốc

Trong Lisp, bạn không nghĩ "lặp N lần" — bạn nghĩ "trường hợp cơ sở là gì, và làm sao để tiến về phía nó?":

(define (factorial n)
  (if (= n 0)
      1
      (* n (factorial (- n 1)))))

(factorial 5)   ; => 120

Và để mọi thứ trực quan hơn, hãy vẽ một thứ gì đó. Racket đi kèm thư viện đồ họa:

#lang racket
(require 2htdp/image)

(define (sierpinski level)
  (if (= level 0)
      (triangle 8 "solid" "purple")
      (let ([t (sierpinski (- level 1))])
        (above t (beside t t)))))

(sierpinski 6)

Dán vào DrRacket, nhấn Run, và xem tam giác Sierpinski hiện ra trên màn hình.

Màn cuối đặc sắc: mã nguồn tự viết mã nguồn

Nhớ dấu nháy đơn ' chứ? Nó biến mã nguồn thành dữ liệu:

'(+ 1 2)          ; => danh sách (+ 1 2), KHÔNG phải số 3
(first '(+ 1 2))  ; => ký hiệu +
(eval '(+ 1 2))   ; => 3. Bạn vừa đánh giá dữ liệu như mã nguồn.

Chương trình của bạn là một danh sách. Bạn có thể tạo danh sách. Do đó: bạn có thể dùng chương trình để tạo ra chương trình. Đây chính là homoiconicity, và vì thế Lisp có macro thực thụ — không phải macro văn bản kiểu C, mà là hàm nhận mã nguồn và trả về mã nguồn khác, trước khi mọi thứ được chạy.

Racket không có vòng lặp while? Tự phát minh một cái:

(define-syntax-rule (while condition body ...)
  (let loop ()
    (when condition
      body ...
      (loop))))

(define counter 0)
(while (< counter 5)
  (displayln counter)
  (set! counter (+ counter 1)))

Bạn vừa mở rộng ngôn ngữ! Trong Lisp, cú pháp thuộc về bạn.

Alan Kay từng gọi Lisp là "phương trình Maxwell của phần mềm": một lõi nhỏ bé mà từ đó mọi thứ khác đều có thể suy ra.

Một lập trình viên đang đọc cuốn sách kinh điểnMột lập trình viên đang đọc cuốn sách kinh điển

Tiếp theo là gì?

  • How to Design Programs — cuốn sách được Racket thiết kế xoay quanh, miễn phí trực tuyến.
  • The Racket Guide — tài liệu chính thức, thuộc hàng tốt nhất hiện có.
  • Beautiful Racket — học cách xây dựng ngôn ngữ của riêng bạn.
  • SICP — kinh điển của kinh điển, nếu bạn muốn sự giác ngộ trọn vẹn.

Minh họa về cấu trúc lập trìnhMinh họa về cấu trúc lập trình

Với cộng đồng lập trình viên Việt Nam đang ngày càng quan tâm đến các ngôn ngữ hàm và tư duy lập trình sâu sắc, Racket là một lựa chọn tuyệt vời để rèn luyện tư duy trừu tượng — một kỹ năng quý giá bất kể bạn làm việc với ngôn ngữ nào sau này. Hãy thử, và biết đâu bạn sẽ tìm thấy sự giác ngộ mà Eric S. Raymond đã nói.

Chia sẻ:FacebookX
Nội dung tổng hợp bằng AI, mang tính tham khảo. Xem bài gốc ↗