Lập trình · Ngôn ngữ lập trình
Hướng dẫn học lập trình Clojure cơ bản cho người mới
Trả lời nhanh
Clojure là một Lisp hàm, kiểu động, chạy trên Java Virtual Machine (với ClojureScript cho JavaScript). Mã được viết dưới dạng danh sách trong ngoặc đơn, dữ liệu mặc định là bất biến, và bạn xây chương trình bằng cách kết hợp các hàm nhỏ trên vector, map và set. Bạn cài Clojure CLI, mở REPL bằng clj, và chạy một tệp bằng clojure -M hello.clj.
Clojure là gì và ai dùng nó
Clojure do Rich Hickey tạo ra và phát hành lần đầu năm 2007. Nó là một phương ngữ Lisp: mọi biểu thức là một danh sách mà phần tử đầu tiên là hàm hoặc toán tử, ví dụ (+ 1 2). Nó biên dịch ra bytecode JVM nên có thể gọi trực tiếp bất kỳ thư viện Java nào. ClojureScript biên dịch cùng ngôn ngữ đó sang JavaScript cho trình duyệt và Node.js.
Clojure được dùng cho dịch vụ web backend, đường ống xử lý dữ liệu và hệ thống nghiệp vụ nội bộ, nhất là ở nơi các nhóm coi trọng dữ liệu bất biến và phát triển tương tác. Lập trình viên thường làm việc trong REPL (vòng lặp đọc-đánh giá-in) kết nối với trình soạn thảo, đánh giá từng biểu thức một khi chương trình đang chạy.
Cài Clojure và chạy Hello, world
Cài môi trường chạy Java (JDK 17 trở lên là lựa chọn an toàn), rồi cài công cụ Clojure CLI từ clojure.org (Homebrew trên macOS, script cài đặt trên Linux, hoặc trình cài đặt cho Windows). CLI cung cấp hai lệnh: clj (REPL có chỉnh sửa dòng) và clojure (để chạy chương trình).
;; hello.clj
(println "Hello, world!")clojure -M hello.clj
# Hello, world!
clj
# user=> (+ 1 2)
# 3Dự án thực tế giữ các thư viện phụ thuộc trong tệp deps.edn và mã nguồn dưới src/, mỗi tệp một namespace. Leiningen (lein) là công cụ build cũ hơn nhưng vẫn phổ biến, dùng tệp project.clj.
Giá trị, kiểu và tên
Clojure có kiểu động. def gắn một tên toàn cục (var) và let gắn các tên cục bộ. Giá trị không đổi sau khi được tạo: không có câu lệnh gán.
| Kiểu | Ví dụ | Ghi chú |
|---|---|---|
| Số nguyên | 42 | long 64 bit; 42N là BigInt |
| Số thực dấu phẩy động | 3.14 | Một double của Java |
| Tỷ số (Ratio) | (/ 1 3) | Phân số chính xác 1/3, không phải 0.333 |
| Chuỗi | "hello" | Một String của Java, chỉ dùng dấu nháy kép |
| Ký tự | \a | Một ký tự đơn |
| Keyword | :name | Nhãn nhanh, tự đánh giá thành chính nó, thường là khóa của map |
| Boolean / nil | true, false, nil | Chỉ false và nil là sai |
(def pi-approx 3.14159)
(let [r 2
area (* pi-approx r r)]
(println "Area:" area))
;; Area: 12.56636
(str "a" 1 :b) ;; => "a1:b"Điều khiển luồng
Trong Clojure mọi thứ là biểu thức trả về giá trị, kể cả if. if nhận đúng một dạng "then" và một dạng "else"; dùng when cho nhiều hiệu ứng phụ mà không có nhánh else, và cond cho nhiều điều kiện.
(defn grade [score]
(cond
(>= score 90) "A"
(>= score 80) "B"
:else "C"))
(println (grade 85)) ;; B
(when (pos? 5) (println "positive") :done)
(doseq [x [1 2 3]] (println (* x x))) ;; 1 4 9
(for [x (range 5) :when (odd? x)] (* x 10)) ;; => (10 30)
(loop [i 0 acc 0]
(if (< i 5) (recur (inc i) (+ acc i)) acc)) ;; => 10Không có vòng lặp for với biến đếm mà bạn thay đổi. doseq chạy hiệu ứng phụ cho từng phần tử, for tạo một dãy lười (list comprehension), và loop/recur diễn tả phép lặp mà không làm ngăn xếp gọi hàm phình ra.
Hàm
Định nghĩa hàm có tên bằng defn. Một hàm có thể có nhiều arity (số đối số khác nhau), và #(...) là dạng ngắn cho hàm ẩn danh nhỏ, trong đó % là đối số.
(defn greet
([] (greet "world"))
([name] (str "Hello, " name "!")))
(greet) ;; => "Hello, world!"
(greet "Ana") ;; => "Hello, Ana!"
(defn sum-all [& nums] (reduce + nums))
(sum-all 1 2 3) ;; => 6
(map #(* % %) [1 2 3]) ;; => (1 4 9)
(filter even? (range 10)) ;; => (0 2 4 6 8)Hàm là giá trị: bạn có thể truyền chúng cho map, filter và reduce, trả về từ hàm khác và lưu trong map.
Collection: danh sách, vector, map và set
Bốn collection lõi của Clojure đều bất biến và bền vững (persistent): "thay đổi" một collection sẽ trả về phiên bản mới dùng chung phần lớn cấu trúc với bản cũ, nên cập nhật rất rẻ.
| Collection | Cú pháp literal | Cách dùng điển hình |
|---|---|---|
| Danh sách | '(1 2 3) | Mã nguồn và dữ liệu tuần tự, thêm vào đầu |
| Vector | [1 2 3] | Dãy có chỉ số, thêm vào cuối |
| Map | {:name "Ana" :age 30} | Bản ghi khóa-giá trị |
| Set | #{:red :green} | Giá trị duy nhất, kiểm tra thành viên |
(def user {:name "Ana" :age 30})
(:name user) ;; => "Ana"
(assoc user :email "[email protected]") ;; new map with :email
(update user :age inc) ;; => {:name "Ana", :age 31}
(conj [1 2] 3) ;; => [1 2 3]
(contains? #{:red :green} :red) ;; => true
(get-in {:a {:b 1}} [:a :b]) ;; => 1
user ;; unchanged: {:name "Ana", :age 30}Tính năng đặc trưng: threading macro, atom và macro
Threading macro giúp đường ống dữ liệu đọc từ trên xuống dưới. -> chèn kết quả trước đó làm đối số đầu tiên của dạng kế tiếp; ->> chèn nó làm đối số cuối cùng.
(->> (range 1 11)
(filter odd?)
(map #(* % %))
(reduce +)) ;; => 165
(def counter (atom 0))
(swap! counter inc)
(swap! counter + 10)
@counter ;; => 11
(defmacro unless [test & body]
`(if ~test nil (do ~@body)))
(unless false (println "runs"))Atom giữ trạng thái có thể thay đổi: swap! áp dụng một hàm một cách nguyên tử, nên dùng an toàn từ nhiều luồng. Ref và agent xử lý các trường hợp đồng thời khác. Vì mã là dữ liệu (danh sách), macro có thể biến đổi mã trước khi biên dịch; when, cond và -> bản thân cũng là macro.
Xử lý lỗi
Clojure dùng ngoại lệ JVM với try, catch và finally. Để gắn dữ liệu vào lỗi, hãy ném ex-info và đọc lại dữ liệu bằng ex-data.
(defn parse-age [s]
(let [n (Long/parseLong s)]
(when (neg? n)
(throw (ex-info "Age must not be negative" {:value n})))
n))
(try
(parse-age "-5")
(catch clojure.lang.ExceptionInfo e
(println (ex-message e) (ex-data e)))
(catch NumberFormatException _
(println "not a number")))
;; Age must not be negative {:value -5}Một chương trình hoàn chỉnh nhỏ: tần suất từ
(ns wordcount
(:require [clojure.string :as str]))
(defn word-counts [text]
(->> (str/split (str/lower-case text) #"\W+")
(remove str/blank?)
frequencies
(sort-by val >)))
(doseq [[word n] (take 2 (word-counts "The cat and the hat and the bat"))]
(println word n))
;; the 3
;; and 2Chương trình chuyển văn bản sang chữ thường, tách theo ký tự không phải ký tự từ bằng literal biểu thức chính quy #"\W+", đếm từng từ bằng frequencies (trả về một map), và sắp xếp các mục theo số lần giảm dần.
Học tiếp ở đâu
- Các hướng dẫn chính thức tại clojure.org: Getting Started, Learn Clojure và hướng dẫn deps.edn.
- Các trang tham chiếu Clojure về cú pháp reader, special form và cấu trúc dữ liệu.
- Thiết lập trình soạn thảo với REPL kết nối (ví dụ Calva cho VS Code, CIDER cho Emacs hoặc Cursive cho IntelliJ).
- Thử ClojureScript nếu bạn muốn nhắm tới trình duyệt.
Câu hỏi thường gặp
Clojure có phải ngôn ngữ hàm không?
Có. Clojure ưu tiên hàm thuần túy và dữ liệu bất biến, nhưng nó thực dụng: cho phép hiệu ứng phụ và cung cấp trạng thái khả biến được quản lý qua atom, ref và agent.
Tôi có cần biết Java để học Clojure không?
Không. Bạn có thể học ngôn ngữ mà không cần Java, nhưng cần cài môi trường chạy Java, và biết chút ít về JVM sẽ giúp khi đọc stack trace hoặc dùng thư viện Java.
Danh sách và vector trong Clojure khác nhau thế nào?
Danh sách '(1 2 3) thêm phần tử vào đầu và chủ yếu dùng cho mã nguồn; vector [1 2 3] thêm vào cuối và hỗ trợ truy cập nhanh theo chỉ số. Phần lớn dữ liệu trong chương trình Clojure được lưu trong vector và map.
Vì sao Clojure dùng nhiều dấu ngoặc đến vậy?
Mã Clojure được viết dưới dạng cấu trúc dữ liệu Lisp, nên mỗi lời gọi là một danh sách. Cú pháp đồng nhất này là điều làm cho macro khả thi, và các trình soạn thảo có chỉnh sửa theo cấu trúc giúp quản lý dấu ngoặc dễ dàng.
Clojure và ClojureScript khác nhau thế nào?
Chúng là cùng một ngôn ngữ trên các môi trường chủ khác nhau: Clojure chạy trên JVM, ClojureScript biên dịch sang JavaScript. Phần tương tác với môi trường chủ khác nhau, nhưng các hàm lõi và cấu trúc dữ liệu thì giống nhau.