Ngôn ngữ lập trình Racket không thú vị vì cú pháp của nó. Điều làm nó thú vị là nó được thiết kế để xây dựng ngôn ngữ, chứ không chỉ viết chương trình.

Bài viết này bỏ qua "hello world" và các bài tập đồ chơi như đoán số, đi thẳng vào những ý tưởng làm nên sự khác biệt của Racket, cùng với một số kỹ thuật viết script hệ thống hữu ích.

Phạm vi của hướng dẫn này: Đây không phải là một tài liệu tham khảo. Nó bao gồm một tập hợp các ý tưởng cốt lõi đã được chọn lọc, như biểu thức, hàm hạng nhất, map/filter/fold, so khớp mẫu, macro, contract, và một số kỹ thuật viết script trên Linux. Để biết thêm chi tiết, hãy đọc Racket Guide và Racket Reference.

Lưu ý về require: Một ví dụ bên dưới có sử dụng (require racket/contract). Nếu file của bạn bắt đầu bằng #lang racket (thay vì #lang racket/base nhỏ hơn), thì dòng này đã được bao gồm sẵn, và việc thêm require một cách tường minh là không cần thiết. Nhiều người vẫn giữ nó để làm rõ file đó phụ thuộc vào những gì.

Thiết lập Môi trường Lập trình Racket

Bạn không cần bất cứ thứ gì cầu kỳ, chỉ cần Racket.

1. Cài đặt Ngôn ngữ Racket

Tải trình cài đặt cho nền tảng của bạn từ download.racket-lang.org (bản phát hành ổn định hiện tại là v9.2). Trên Linux, bạn cũng có thể cài đặt qua trình quản lý gói, nhưng trình cài đặt chính thức thường cung cấp phiên bản mới hơn.

Chúng tôi đã xuất bản một hướng dẫn riêng giải thích các cách khác nhau để cài đặt Racket trên Linux. Hãy xem liên kết sau:

Sau khi cài đặt, hãy xác minh phiên bản Racket:

racket --version

2. Hai cách để chạy các ví dụ mã bên dưới

Tùy chọn A: dưới dạng file script (hầu hết các ví dụ trong hướng dẫn này đều giả định cách này):

Lưu một đoạn mã vào file, bắt đầu bằng dòng #lang racket như trong các ví dụ có kèm theo (một số đoạn mã là các mảnh vụn được thiết kế để dán vào một file có sẵn, vì vậy hãy tự thêm #lang racket làm dòng đầu tiên nếu nó bị thiếu):

#lang racket
(displayln (+ 2 3))

Sau đó chạy nó từ terminal:

racket my-file.rkt

Tùy chọn B: trong REPL (nhanh nhất để thử các biểu thức đơn lẻ):

racket

Thao tác này sẽ đưa bạn vào một dấu nhắc tương tác (>), nơi bạn có thể dán bất kỳ biểu thức đơn lẻ nào từ hướng dẫn này và thấy kết quả result ngay lập tức:

> (map add1 '(1 2 3))
'(2 3 4)

Gõ (exit) hoặc nhấn Ctrl++D để thoát khỏi REPL.

3. Tùy chọn: DrRacket (IDE đi kèm)

Trình cài đặt cũng bao gồm DrRacket, một trình soạn thảo đồ họa có nút "Run" tích hợp và một khung tương tác bên dưới mã của bạn. Đây thường là cách dễ nhất để thử nghiệm trong khi làm việc với các ví dụ, đặc biệt là các ví dụ nhiều dòng. Chỉ cần mở nó, dán một đoạn mã có #lang racket ở trên cùng và nhấp Run.

Hãy bắt đầu.

1. Mã là Dữ liệu

Không giống như C hay Python, mã nguồn Racket là một cấu trúc dữ liệu, cụ thể là các danh sách lồng nhau. Ví dụ, (+ 2 3)

(+ 2 3)

thực chất là một danh sách có phần tử đầu tiên là ký hiệu + (tham chiếu đến một hàm), và phần còn lại là các đối số: +, 2, 3.

Bởi vì mã là dữ liệu, bạn có thể xây dựng nó theo chương trình và eval nó. Lưu ý rằng ví dụ này cần chạy trong REPL, không phải dưới dạng file script. eval sử dụng không gian tên hiện tại, và một script chạy với #lang racket bắt đầu với một không gian tên trống, vì vậy điều này sẽ gây ra lỗi error unbound-identifier thay vì trả về 30.

(define expr '(+ 10 20))
(eval expr)

Kết quả mẫu:

30

Nếu bạn muốn chạy cái này dưới dạng script thay vì trong REPL, hãy cung cấp cho eval một không gian tên tường minh:

#lang racket
(define expr '(+ 10 20))
(eval expr (make-base-namespace))

Sao chép mã trên vào một file, ví dụ my-file.rkt và chạy nó bằng lệnh:

racket my-file.rkt

Điều này sẽ in ra 30.

Ý tưởng đơn lẻ này, tức là mã như dữ liệu, là thứ làm cho macro trở nên khả thi sau này.

2. Mọi thứ đều là Biểu thức

Không giống như C, C++, hay Python, hầu như mọi dạng Racket đều trả về một giá trị. Điều này đúng ngay cả với if.

# Python
if x > 10:
y = 5
else:
y = 20

trở thành

(define y (if (> x 10) 5 20))

Theo cách tương tự, begin chạy một danh sách các biểu thức theo thứ tự và trả về giá trị của biểu thức cuối cùng.

(define result
(begin
(displayln "Calculating...")
(+ 4 7)))

Kết quả mẫu:

Calculating...

result bây giờ là 11. Bởi vì mọi biểu thức đều trả về một giá trị, bạn có thể kết hợp các biểu thức trực tiếp thay vì tạo các biến phụ chỉ để giữ một giá trị cho một bước trước khi sử dụng nó.

3. Hàm là Giá trị Hạng nhất

Các hàm có thể được lưu trữ, truyền đi và trả về như bất kỳ giá trị nào khác.

(define (square x) (* x x))
(define f square)
(f 8)
64

Các hàm ẩn danh hoạt động theo cùng một cách:

(map (lambda (x) (* x x)) '(1 2 3 4 5))

Kết quả mẫu:

'(1 4 9 16 25)

Bạn thậm chí có thể đặt các hàm vào một danh sách:

(list add1 sub1 sqrt)
'(#<procedure:add1> #<procedure:sub1> #<procedure:sqrt>)

Lưu ý: văn bản chính xác hiển thị cho mỗi thủ tục có thể hơi khác nhau tùy thuộc vào phiên bản Racket của bạn, nhưng nó sẽ luôn hiển thị rằng danh sách chứa các hàm, không phải số hay văn bản.

4. Lập trình Bậc cao: map, filter, fold

Thay vì viết các vòng lặp loop tường minh, hãy suy nghĩ theo các phép biến đổi trên tập hợp. Ba thao tác này giải quyết một số lượng đáng ngạc nhiên các vấn đề.

Map - biến đổi mọi phần tử:

(map string-length '("Linux" "Kernel" "Racket"))

Kết quả mẫu:

'(5 6 6)

Filter - giữ lại các phần tử khớp với một vị từ:

(filter even? '(1 2 3 4 5 6 7 8))

Kết quả mẫu:

'(2 4 6 8)

Fold - kết hợp các phần tử thành một giá trị result duy nhất:

(foldl + 0 '(1 2 3 4 5))    ; sum      -> 15
(foldl * 1 '(1 2 3 4))      ; product  -> 24
(foldl max 0 '(2 8 5 12 4)) ; maximum  -> 12

5. Đệ quy và Lời gọi Đuôi

Racket có các dạng vòng lặp for và do, nhưng nhiều chương trình Racket theo thành ngữ sử dụng đệ quy thay vì vòng lặp loop, đặc biệt là khi làm việc với danh sách.

(define (factorial n)
(if (zero? n)
1
(* n (factorial (sub1 n)))))
(factorial 5)

Kết quả mẫu:

120

Racket cũng đảm bảo các lời gọi đuôi thích hợp. Điều này có nghĩa là khi một hàm gọi chính nó như bước cuối cùng, Racket biến lời gọi đó thành một vòng lặp loop thay vì thêm một lớp mới vào ngăn xếp lời gọi. Nhờ đó, hàm dưới đây có thể xử lý hàng triệu phần tử một cách an toàn mà không bị hết bộ nhớ.

(define (sum lst)
(define (loop lst acc)
(if (empty? lst)
acc
(loop (rest lst) (+ acc (first lst)))))
(loop lst 0))

6. Named Let (Đệ quy Đọc như Vòng lặp)

let loop cho phép bạn viết đệ quy lặp mà không cần một hàm cấp cao nhất riêng biệt:

(let loop ([i 0])
(when (< i 5)
(displayln i)
(loop (add1 i))))

Kết quả mẫu (mỗi lần gọi displayln in một dòng riêng):

0
1
2
3
4

Cái này trông giống như một vòng lặp loop, nhưng bên dưới nó thực sự là đệ quy. Bởi vì lời gọi đến loop ở vị trí đuôi, Racket tự động biến nó thành một vòng lặp loop thực sự.

7. So khớp Mẫu

So khớp mẫu là một trong những tính năng tốt nhất của Racket. Nó đến từ racket/match, đã được bao gồm sẵn khi bạn sử dụng #lang racket, vì vậy bạn không cần thêm dòng require nào khác.

Giải cấu trúc đơn giản:

(match '(1 2 3)
[(list a b c) (+ a b c)])

Kết quả mẫu:

6

Cấu trúc lồng nhau:

(match '((1 2) (3 4))
[(list (list a b) (list c d)) (+ a b c d)])

Kết quả mẫu:

10

So khớp trên một "hình dạng" lệnh. Điều này thực sự hữu ích để phân tích cú pháp file cấu hình hoặc đối số dòng lệnh:

(match '(cp file1 file2)
[(list 'cp src dst) (printf "Copy ~a -> ~a\n" src dst)]
[_ "Unknown"])

Kết quả mẫu:

Copy file1 -> file2

8. Nhiều Giá trị Trả về

Racket hỗ trợ sẵn một hàm trả về nhiều hơn một giá trị, mà không cần đóng gói chúng vào một tuple hay danh sách.

(define (divide x y)
(values (quotient x y) (remainder x y)))
(let-values ([(q r) (divide 20 3)])
(displayln q)
(displayln r))

Kết quả mẫu:

6
2

9. List Comprehension

List comprehension của Python trông giống như [x*x for x in nums], trong đó nums là một danh sách các số. Tương đương của Racket là for/list:

(for/list ([x '(1 2 3 4)])
(* x x))

Kết quả mẫu:

'(1 4 9 16)

Bạn có thể thêm một mệnh đề filter bằng cách sử dụng #:when:

(for/list ([i (in-range 20)] #:when (even? i))
(* i i))

Kết quả mẫu:

'(0 4 16 36 64 100 144 196 256 324)

10. Bảng băm

(define h (hash "cpu" 16 "ram" 32))
(hash-ref h "cpu")

Kết quả mẫu:

16

Quan trọng: (hash ...) tạo một bảng băm bất biến. hash-set không thay đổi h tại chỗ. Thay vào đó, nó trả về một bảng băm hoàn toàn mới:

(define h2 (hash-set h "disk" 1000))

h vẫn giữ nguyên. h2 là bảng mới. Nếu bạn muốn thay đổi một bảng băm tại chỗ, hãy sử dụng make-hash và hash-set!:

(define mh (make-hash))
(hash-set! mh "disk" 1000)

11. Cấu trúc

Một struct là một bản ghi nhẹ, bất biến theo mặc định. Nó là câu trả lời của Racket cho các lớp đơn giản.

(struct person (name age))
(define p (person "Senthil" 40))
(person-name p) ; "Senthil"
(person-age p)  ; 40

12. Contract

Contract cho phép bạn ghi lại và thực thi các kỳ vọng của một hàm tại thời gian chạy. Chúng là một giải pháp thay thế thực tế cho một hệ thống kiểu tĩnh riêng biệt, đặc biệt là tại các ranh giới module.

(require racket/contract)
(define/contract (divide a b)
(-> number? (and/c number? (not/c zero?)) number?)
(/ a b))

Gọi (divide 5 0) sẽ gây ra một vi phạm contract trước khi phép chia h xảy ra. Thông báo lỗi error hiển thị rõ ràng đối số nào đã vi phạm quy tắc nào.

Kết quả mẫu:

divide: contract violation
expected: (not/c zero?)
given: 0
in: an and/c case of
the 2nd argument of
(->
number?
(and/c number? (not/c zero?))
number?)
contract from: (function divide)
blaming: top-level
(assuming the contract is correct)
at: string:1:18
[,bt for context]

13. Macro: Siêu năng lực thực sự của Racket

Một hàm thông thường eval các đối số của nó trước khi nó chạy.

(define (twice x) (+ x x))

Một macro thì khác. Nó biến đổi mã nguồn trước khi mã đó được eval. Các biểu thức đối số không bao giờ được eval ngay từ đầu.

(define-syntax-rule (unless-again test body)
(if (not test) body (void)))
(unless-again (> 2 5) (displayln "Hello"))

Kết quả mẫu:

Hello

Đây là một ví dụ cổ điển khác. Nó chạy một biểu thức hai lần và biểu thức đó vẫn chưa được eval cho đến khi macro mở rộng nó.

(define-syntax-rule (twice-run expr)
(begin expr expr))
(twice-run (displayln "Hello"))

Kết quả mẫu:

Hello
Hello

Đây là lý do tại sao Racket nổi tiếng trong giới ngôn ngữ lập trình. Macro cho phép bạn mở rộng chính ngôn ngữ, không chỉ gọi các hàm được viết trong đó.

14. Xây dựng DSL

Hãy tưởng tượng một file cấu hình trông như thế này:

(server
port 8080
ssl yes
workers 8)

Cú pháp này không tồn tại trong Racket theo mặc định. Bạn có thể tự xây dựng nó, sử dụng define-syntax-rule, hoặc các dạng tổng quát hơn syntax-rules và syntax-parse. Đây là một ví dụ nhỏ:

(define-syntax-rule (repeat n body)
(for ([i n]) body))
(repeat 5 (displayln "Ostechnix"))

Kết quả mẫu:

Ostechnix
Ostechnix
Ostechnix
Ostechnix
Ostechnix

Cái này đọc giống như một tính năng ngôn ngữ có sẵn, bởi vì trong Racket bạn thực sự có thể thêm các tính năng ngôn ngữ mới. Rất ít ngôn ngữ chính thống làm được điều này dễ dàng và thú vị như vậy.

15. Tính toán Lười và Stream

Racket là eager (còn gọi là strict) theo mặc định. Điều này có nghĩa là nó thường tính toán các giá trị ngay lập tức. Nhưng Racket cũng có hỗ trợ sẵn cho tính lười thông qua racket/stream.

(require racket/stream)
(define nums (stream 1 2 3 4 5))
(stream-first nums)

Kết quả mẫu:

1

Stream có thể là vô hạn, bởi vì các phần tử của chúng chỉ được tính toán khi bạn thực sự yêu cầu chúng:

(define ones (stream-cons 1 ones))
(stream-ref ones 1000000)

Kết quả mẫu:

1

Chỉ phần của stream mà bạn thực sự yêu cầu mới được tính toán.

16. Viết Script Hệ thống trên Linux

Racket cũng có thể hoạt động như một ngôn ngữ viết script shell và xử lý văn bản rất có năng lực.

Đọc file:

(file->string "/proc/cpuinfo")

hoặc từng dòng một:

(with-input-from-file "/proc/meminfo"
(lambda ()
(displayln (read-line))))

Chạy lệnh shell:

(system "ls -lh")

Thu thập đầu ra thay vì in nó:

(with-output-to-string
(lambda () (system "uname -r")))

Một bộ lọc /proc/meminfo nhỏ:

#lang racket
(define text (file->string "/proc/meminfo"))
(for ([line (string-split text "\n")])
(when (regexp-match? #rx"Mem" line)
(displayln line)))

Điều này in ra các dòng khớp đầy đủ, ví dụ:

MemTotal:       11719360 kB
MemFree:         6641244 kB
MemAvailable:    9034448 kB

(Các con số chính xác phụ thuộc vào máy của bạn. Các hàm này khớp và in toàn bộ dòng, không chỉ nhãn.)

Chương trình dòng lệnh:

#lang racket
(command-line #:args (name)
(printf "Hello ~a\n" name))

Chạy nó như thế này:

racket hello.rkt Racket

Kết quả mẫu:

Hello Racket

Một pipeline hàm. Cái này đọc một file chứa các số, bỏ qua các dòng trống, chuyển đổi văn bản thành số và cộng tất cả chúng lại:

(define total
(foldl +
0
(map string->number
(filter (lambda (s) (not (string=? s "")))
(string-split (file->string "numbers.txt") "\n")))))

Một khi map, filter và foldl trở nên tự nhiên, các pipeline như thế này đọc gần giống như một mô tả đơn giản về nhiệm vụ thay vì một tập hợp các bước triển khai.

Một máy chủ web nhỏ. Racket đi kèm với một thư viện ứng dụng web đầy đủ:

#lang racket
(require web-server/servlet
web-server/servlet-env)
(define (my-app req)
(response/xexpr '(html (body "Hello from Racket"))))
(serve/servlet my-app #:servlet-regexp #rx"")

Một trình xử lý phải trả về một giá trị phản hồi thích hợp, không phải một chuỗi đơn thuần, vì vậy phiên bản này bọc văn bản bằng response/xexpr. Tùy chọn #:servlet-regexp #rx"" cũng cần thiết để máy chủ trả lời tại địa chỉ đơn giản bên dưới, thay vì chỉ ở đường dẫn mặc định của nó.

Sao chép mã trên vào một file, ví dụ http.rkt và chạy nó:

$ racket http.rkt
Your Web application is running at http://localhost:8000/servlets/standalone.rkt.
Stop this program at any time to terminate the Web Server.

Bây giờ, bạn sẽ thấy máy chủ web đang chạy tại http://localhost:8000/. Một máy chủ web hoạt động chỉ trong vài dòng.

Nhấn CTRL+C để dừng máy chủ web.

17. cond (Câu điều kiện Nhiều nhánh)

if chỉ xử lý hai nhánh. cond xử lý nhiều nhánh và là thứ mà mã Racket thực tế sử dụng thay vì các if lồng nhau.

(define (sign n)
(cond
[(> n 0) "positive"]
[(< n 0) "negative"]
[else "zero"]))
(sign -5)

Kết quả mẫu:

"negative"

Mỗi dòng được kiểm tra theo thứ tự. Phép thử đúng đầu tiên sẽ thắng và else bắt tất cả phần còn lại else.

18. let và let*

let tạo các biến cục bộ. Tất cả các giá trị được tính toán trước, vì vậy một biến không thể sử dụng biến khác.

(let ([x 1] [y 2])
(+ x y))

Kết quả mẫu:

3

let* cho phép mỗi biến sử dụng các biến được định nghĩa trước nó.

(let* ([x 1] [y (+ x 1)])
(+ x y))

Kết quả mẫu:

3

Viết (y (+ x 1)) bên trong let thông thường sẽ thất bại, bởi vì x chưa sẵn sàng.

19. Vector

Danh sách tốt nhất để thêm các mục từng cái một từ phía trước. Vector tốt nhất khi bạn cần truy cập acc nhanh theo chỉ mục và chúng có thể được thay đổi tại chỗ.

(define v (vector 1 2 3))
(vector-ref v 0)

Kết quả mẫu:

1
(vector-set! v 0 99)
(vector-ref v 0)

Kết quả mẫu:

99

20. Xử lý Lỗi Cơ bản

error ném ra một ngoại lệ. with-handlers bắt nó, để chương trình của bạn có thể phản hồi thay vì bị sập.

(with-handlers ([exn:fail? (lambda (e) (displayln (exn-message e)))])
(error "Something went wrong"))

Kết quả mẫu:

Something went wrong

Kết luận

Bạn đã thấy các ý tưởng cốt lõi định hình cách mã Racket được viết: biểu thức và hàm hạng nhất, map/f Chúc may mắn!