- Clojure không phải là một trong những ngôn ngữ lập trình chủ lưu, và với một số người thì có thể còn khá xa lạ
- Những ưu điểm chính của Clojure
- Năng suất của lập trình viên: Clojure mang lại môi trường phát triển có tính tương tác cao, ít việc lặp lại và hiệu quả. Lập trình viên có thể hài lòng hơn và đưa sản phẩm ra thị trường nhanh hơn
- Khả năng bảo trì dài hạn: ngôn ngữ Clojure và hệ sinh thái của nó đã trưởng thành và ổn định. Có thể xây dựng hệ thống chất lượng cao trong khi giảm chi phí bảo trì
- Văn hóa lấy ý tưởng làm trung tâm: cộng đồng Clojure khám phá các ý tưởng từ quá khứ đến hiện tại, từ học thuật đến công nghiệp, để tìm ra cách phát triển phần mềm tốt hơn. Điều này mang lại cho lập trình viên những thử thách mới và cơ hội học hỏi
(hello 'clojure)
- Clojure là một ngôn ngữ thuộc họ Lisp, vốn được tạo ra từ những năm 1950
- Lisp khởi đầu như một mô hình lý thuyết, nhưng trong lập trình thực tế cũng mang lại mức độ thanh nhã khái niệm rất cao
- Cú pháp của mã trùng khớp với cấu trúc dữ liệu, nên có nhiều ưu điểm so với các ngôn ngữ có cú pháp phức tạp truyền thống
- Từng được dùng làm ngôn ngữ chủ lực trong thời kỳ bùng nổ AI trước đây
- Các ngôn ngữ họ Lisp từng nhiều lần nổi lên rồi lại mờ đi, nhưng trong khoảng 10 năm gần đây Clojure đã thu hút được nhiều chú ý
- Clojure chạy trên JVM của Java và phản ánh nhiều khái niệm lập trình hiện đại
- Hỗ trợ mạnh cho cấu trúc dữ liệu bất biến
- Được thiết kế với tính đồng thời ngay từ đầu
- Dù là một cộng đồng nhỏ phát triển mà không có sự hậu thuẫn từ các tập đoàn lớn, Clojure vẫn sở hữu những đặc tính ngôn ngữ rất mạnh
- Clojure có thể chạy trong nhiều môi trường khác nhau
- ClojureScript: biên dịch sang JavaScript để chạy
- ClojureCLR: chạy trong môi trường .NET
- Babashka: trình thông dịch scripting tốc độ cao sử dụng GraalVM
- Jank: hỗ trợ biên dịch native
- Khi học Clojure, bạn có thể tận dụng cùng một nền tảng kiến thức trên nhiều môi trường khác nhau
Phương thức phát triển tương tác
- Lập trình là quá trình lặp lại giữa viết mã và kiểm chứng
- Nếu không có phản hồi, rất khó chắc chắn rằng mã có hoạt động đúng hay không
- Các cách phản hồi phổ biến:
- Chạy script lặp đi lặp lại
- Tương tác UI và xuất log
- Sử dụng unit test
- Dùng compiler và công cụ phân tích tĩnh
- Phản hồi nhanh ảnh hưởng rất lớn đến năng suất của lập trình viên
- Phản hồi càng chậm thì thời gian debug càng tăng, còn thời gian viết mã lại giảm
- Ngoài các cách phản hồi truyền thống, Clojure còn cung cấp phương thức phát triển tương tác (interactive development)
- Cốt lõi là phát triển dựa trên REPL (Read-Eval-Print Loop)
- Trước khi viết mã, lập trình viên khởi chạy runtime Clojure và kết nối nó với editor
- Có thể chạy từng mảnh mã một và nhận phản hồi ngay lập tức
- Nhờ cấu trúc cú pháp nhất quán của Lisp, có thể tự do chạy từ những đoạn mã nhỏ đến các khối mã lớn
- Khác với REPL thông thường, Clojure cho phép chạy các đoạn mã trong khi đang kết nối với một hệ thống đang vận hành
- Cách này tạo ra vòng phản hồi mạnh hơn rất nhiều so với kiểu truyền thống “viết mã → chạy → debug”
- Có thể sửa đổi và debug chương trình theo thời gian thực
- Không chỉ là một REPL đơn giản, mà là công cụ tương tác mạnh mẽ có thể dùng cả trong môi trường production
Văn hóa coi trọng tính ổn định
- Khi chọn Clojure, bạn không chỉ có được một công nghệ mạnh mà còn trở thành một phần của cộng đồng với triết lý và nguyên tắc riêng
- Nếu chỉ áp dụng công nghệ mà không giao lưu với cộng đồng thì rất khó trải nghiệm giá trị thực sự của Clojure
- Cộng đồng Clojure xem tính ổn định và khả năng tương thích ngược là những giá trị quan trọng
- Ngôn ngữ cốt lõi được cải tiến và mở rộng liên tục gần như không bao giờ có thay đổi phá vỡ tương thích
- Hệ sinh thái mã nguồn mở của Clojure cũng đi theo cùng triết lý đó, giảm thiểu các thay đổi không cần thiết (churn)
- Điều này tương phản với phần lớn hệ sinh thái ngôn ngữ lập trình hiện đại khác
- Lãng phí tài nguyên do những thay đổi không cần thiết gây ra trên quy mô toàn cầu lên đến hàng chục tỷ USD
- Trong Clojure, việc nâng cấp lên phiên bản mới nhất của ngôn ngữ và thư viện là điều rất tự nhiên
- Có thể hưởng lợi từ sửa lỗi, cập nhật bảo mật và cải thiện hiệu năng mà không cần viết lại codebase
- Ở nhiều ngôn ngữ khác, mã cũ có thể bị hỏng khi phiên bản mới ra mắt, còn trong Clojure thì vẫn được giữ nguyên
- Một số lập trình viên có thể đặt câu hỏi: “Làm sao phát triển mà không thay đổi?”
- Nhưng ổn định không đồng nghĩa với trì trệ
- Clojure phát triển bằng cách bổ sung và cải tiến tính năng mới mà không phá vỡ những gì đang có
- Lập trình viên có thể liên tục dùng công cụ tốt hơn mà không phải sửa mã vô ích
Hệ thống thông tin và biểu diễn tri thức
- Trong phát triển ứng dụng web và ứng dụng doanh nghiệp, thu thập, truy cập và xử lý thông tin là trọng tâm
- Tuy nhiên, các ngôn ngữ lập trình chủ lưu thường có thiết kế kém hiệu quả về mặt biểu diễn và thao tác thông tin
- Ép buộc dùng cấu trúc dữ liệu cấp thấp, gây ra độ phức tạp không cần thiết
- Hệ thống kiểu tĩnh quá cứng nhắc khiến việc thao tác dữ liệu linh hoạt trở nên khó khăn
- Clojure cung cấp sẵn cấu trúc dữ liệu hàm và khả năng thao tác dữ liệu mạnh mẽ
- Là ngôn ngữ kiểu động, nó tuân theo “Open World Assumption”
- Đây là cách tiếp cận tối đa hóa khả năng mở rộng và tính linh hoạt của dữ liệu
- Chịu ảnh hưởng mạnh từ RDF (framework mô hình hóa dữ liệu cho Semantic Web)
- Ví dụ tiêu biểu là có độ cộng hưởng rất tốt với cơ sở dữ liệu đồ thị như Datomic
- Có thể gán ý nghĩa độc lập với ngữ cảnh bằng cách dùng keyword có namespace
- Cấu trúc Map của Clojure với keyword có namespace cho phép biểu đạt ý nghĩa tinh vi hơn so với JSON đơn thuần
- Dễ mở rộng dữ liệu hơn, đồng thời tránh xung đột tên mà vẫn biểu diễn dữ liệu trực quan
Các hàm nhỏ có thể kết hợp và dữ liệu bất biến
- Trong Clojure, việc lập trình xoay quanh hàm thuần (pure functions) và dữ liệu bất biến (immutable data) là điều phổ biến
- Bạn vẫn có thể viết mã mệnh lệnh kiểu Java, Ruby hay C, nhưng mã Clojure đúng chất (idiomatic Clojure) thì rất khác
- Hàm thuần: chỉ dựa trên đầu vào để trả về kết quả và không thay đổi trạng thái bên ngoài
- Dữ liệu bất biến: mang ý nghĩa dựa trên giá trị (value) thay vì tham chiếu (reference) hay danh tính đối tượng (identity)
- Vì không bị ảnh hưởng bởi trạng thái bên ngoài nên mã có tính dự đoán cao hơn
- Không có chuyện sửa biến toàn cục hay phát sinh side effect ngoài dự kiến
- Do không có nguy cơ dữ liệu bị thay đổi, việc xử lý song song và giải quyết bài toán đồng thời trở nên dễ dàng hơn
Xử lý đồng thời
- Điện toán hiện đại vận hành trên nền tảng bộ xử lý đa lõi, và tính đồng thời là yếu tố thiết yếu
- Khi định luật Moore chạm giới hạn, khai thác song song là chìa khóa để tăng hiệu năng
- Nhưng khi xử lý trạng thái khả biến (mutable state) thì cần giải quyết bài toán đồng bộ hóa và điều khiển thời điểm rất phức tạp
- Clojure nhấn mạnh tính bất biến (immutability) để giải quyết bài toán đồng thời từ gốc
- Khi thao tác trên bộ nhớ khả biến, sẽ phát sinh sự phụ thuộc vào thời điểm và thứ tự
- Mô hình biến đổi dữ liệu thuần túy (data-in, data-out) luôn có thể chạy song song một cách an toàn
- Clojure tận dụng các tính năng đồng thời sẵn có của JVM (
java.util.concurrent) nhưng đồng thời cung cấp các công cụ trừu tượng hóa ở mức cao hơn
- Atoms: hỗ trợ thao tác nguyên tử bằng CAS (Compare-and-Set) và tự động retry
- Refs: cung cấp Software Transactional Memory (STM)
- Agents: áp dụng cập nhật theo cách bất đồng bộ
- Futures: cung cấp giao diện fork-and-join dựa trên thread pool
- Có thư viện core.async
- Hỗ trợ mẫu CSP (Communicating Sequential Processes) tương tự goroutine của Go
- Có thể so sánh với mô hình Actor của Erlang/Elixir hoặc Akka của Scala
- Ngay cả khi không dùng các khả năng trừu tượng hóa cấp cao của Clojure, vẫn có thể dùng các kỹ thuật điều khiển đồng thời cấp thấp hơn
- Hỗ trợ hàng đợi đồng bộ, tham chiếu nguyên tử, lock, semaphore, nhiều kiểu thread pool và quản lý thread thủ công
- Khi cần có thể kiểm soát đồng thời ở mức chi tiết, nhưng trong đa số trường hợp dùng công cụ trừu tượng hóa sẽ an toàn và hiệu quả hơn
Local Reasoning
- Độ phức tạp của lượng mã có thể cân nhắc cùng lúc luôn có giới hạn
- Nếu trạng thái và thay đổi của chương trình phát sinh ở nhiều nơi, việc hiểu và bảo trì mã sẽ trở nên khó khăn
- Lý do Clojure giúp Local Reasoning dễ dàng hơn
- Mã xoay quanh hàm thuần (pure function)
- Có thể hiểu hoàn toàn hành vi của hàm chỉ từ giá trị đầu vào
- Không cần cân nhắc trạng thái bên ngoài hàm
- Khác biệt so với ngôn ngữ hướng đối tượng
- Clojure giảm thiểu polymorphism để dễ xác định mã đang chạy ở đâu
- Trong lập trình hướng đối tượng (OOP), “mọi thứ diễn ra ở nơi khác”, còn trong Clojure thì chỉ cần lần theo các hàm được định nghĩa trong namespace
- Nhờ cấu trúc cú pháp nhất quán của Clojure, việc refactor mã trở nên thuận tiện
- Việc dùng dữ liệu bất biến và hàm thuần giúp giảm side effect bất ngờ khi thay đổi mã
- Có thể tách riêng phần mã mệnh lệnh ở mức tối thiểu để kết hợp hài hòa mã mệnh lệnh và mã hàm
Kiểm thử dễ dàng
- Với mã dựa trên hàm thuần của Clojure, chỉ cần đưa đầu vào và kiểm tra đầu ra là đã có thể test
- Không cần khởi tạo trạng thái phức tạp, cấu hình mock object hay điều khiển timing cho việc kiểm thử
- Nhờ đó độ tin cậy của test cao hơn và ít bị “flakiness” hơn
- Hỗ trợ kỹ thuật kiểm thử nâng cao là Property Based Testing (Generative Testing)
- Sinh đầu vào ngẫu nhiên để tìm các trường hợp vi phạm thuộc tính hoặc bất biến nhất định
- Hỗ trợ kỹ thuật shrinking để tìm ra ca lỗi tối thiểu
- Khái niệm này bắt nguồn từ Haskell và đã được hiện thực trong nhiều ngôn ngữ qua các framework kiểm thử kiểu QuickCheck
- Nó còn hiệu quả hơn nhờ cấu trúc dữ liệu bất biến và phương thức phát triển dựa trên REPL của Clojure
Lợi thế khi tuyển dụng lập trình viên Clojure
- Nhìn chung, so với các ngôn ngữ phổ biến như JavaScript hay Python, số lượng lập trình viên Clojure ít hơn tương đối
- Nhưng vì lập trình viên Clojure ít, số doanh nghiệp dùng Clojure cũng không nhiều
- Do đó cân bằng cung cầu tổng thể vẫn được giữ
- Trên thực tế, doanh nghiệp cần lập trình viên Clojure và các lập trình viên này thường vẫn tìm thấy nhau khá phù hợp
- Lập trình viên Clojure thường có năng lực giải quyết vấn đề ở mức cao
- Thay vì chỉ học một công nghệ đang thịnh hành, họ thường là những người thích khám phá cách tư duy và ý tưởng mới
- Trên thực tế, các công ty dùng Clojure đánh giá rằng dù số lượng ứng viên không nhiều, chất lượng lại rất cao
- Ví dụ: Nubank đã trực tiếp đào tạo hàng trăm lập trình viên ở Brazil bằng Clojure và tạo nên một trường hợp thành công tiêu biểu
- Tìm lập trình viên Clojure là việc khó, nhưng nếu tìm được đúng người thì khả năng cao sẽ có được một lập trình viên xuất sắc
- Thay vì chỉ tìm người đã có kinh nghiệm với ngôn ngữ, đào tạo những lập trình viên có khả năng học hỏi tốt cũng là một lựa chọn hay
- Chính cộng đồng Clojure có xu hướng thu hút những lập trình viên thích suy nghĩ sâu và giải quyết vấn đề
Trade-off và điều chỉnh mức độ trừu tượng
- Clojure là ngôn ngữ high-level, hướng đến việc viết mã ngắn gọn và giàu khả năng biểu đạt
- Nhờ cấu trúc dữ liệu hàm (bất biến) và API thao tác dữ liệu mạnh mẽ, mức độ phức tạp không cần thiết được giảm đi
- Cấu trúc dữ liệu của Clojure bên trong sử dụng cấu trúc cây (Hash Array Mapped Trie) để đảm bảo tính bất biến
- Khi cập nhật sẽ xảy ra path copying, nên gánh nặng GC (garbage collection) có thể tăng lên
- Trong quá trình interop với Java cũng có thể phát sinh runtime reflection, boxing/unboxing
- Trong hầu hết ứng dụng thông thường, chi phí này gần như không đáng kể, trong khi lợi ích tăng năng suất phát triển lại rất lớn
- Với các trường hợp cần hiệu năng cao như engine đồ họa thời gian thực, xử lý tín hiệu hay tính toán số, vẫn có thể tối ưu ở mức thấp
- Trong Clojure có thể dùng type hint để loại bỏ reflection và tối ưu phép toán trên kiểu nguyên thủy
- Có thể tận dụng cache CPU bằng cách dùng mảng kiểu nguyên thủy liên tiếp
- Có thể sử dụng thư viện tăng tốc GPU
- Phần lớn ngôn ngữ high-level khi cần hiệu năng quan trọng thường phải dùng extension native bằng C/Rust, nhưng
Clojure có thể giải quyết phần lớn vấn đề hiệu năng nhờ tận dụng tối ưu hóa của JVM (biên dịch JIT)
- Thông qua profiling và một chút tối ưu, có thể đẩy hiệu năng của các thành phần như event loop lên mức rất cao
Metaprogramming và API lấy dữ liệu làm trung tâm
- Là ngôn ngữ họ Lisp, Clojure có thể xử lý code như dữ liệu
- Tương tự JSON nhưng có thể biểu diễn chương trình bằng cấu trúc dễ đọc hơn
- Cung cấp cách biểu diễn dữ liệu giống JSON thông qua định dạng EDN (Extensible Data Notation)
- Clojure cung cấp khả năng biến đổi chính code bằng macro
- Tuy nhiên, cộng đồng Clojure có văn hóa hạn chế dùng macro một cách thận trọng
- Macro khó debug và có thể kém tương thích với các công cụ phân tích tĩnh
- Thay vào đó, cách thiết kế API data-driven được ưa chuộng hơn
- Ví dụ: HTTP routing, tạo HTML/CSS, kiểm chứng dữ liệu
- Thay vì gọi trực tiếp một hàm cụ thể, hành vi được chỉ định bằng cấu trúc dữ liệu (map, vector)
- API dựa trên dữ liệu có thể thao tác động, đồng thời dễ lưu và chỉnh sửa cấu hình người dùng
- Nhờ API lấy dữ liệu làm trung tâm, có thể tái cấu hình hệ thống một cách động trong runtime
- Qua đó có thể dễ dàng hiện thực hệ thống mô phỏng cực kỳ linh hoạt, quản lý cấu hình động và metaprogramming
Interop với Java và tận dụng hệ sinh thái
- Phát triển ứng dụng hiện đại là quá trình kết hợp vô số thư viện và API mã nguồn mở
- Vì Clojure chạy trên JVM, nó có thể tận dụng hàng triệu package Java trên Maven Central
- Có thể gọi thư viện Java một cách đơn giản mà không cần reflection
- So với Java, Clojure có mã ngắn gọn hơn nhiều và thuận tiện cho lập trình mang tính thử nghiệm bằng REPL
- Có thể khám phá và kết hợp API nhanh hơn rất nhiều so với Java
- Với ClojureScript, có thể tận dụng hệ sinh thái JavaScript và NPM theo cùng cách đó
Văn hóa lấy ý tưởng làm trung tâm
- Dùng ngôn ngữ nào cũng có thể tạo ra dự án thành công, và ngược lại, nếu dùng sai cách thì ngôn ngữ nào cũng có thể thất bại
- Công cụ tốt không tự động tạo ra lập trình viên giỏi
- Clojure khó nhập môn hơn và cần trải qua thử-sai trong quá trình học, nhưng chính điều đó cho phép tư duy sâu hơn
- Một số dự án Clojure vẫn còn giữ cách viết mã kiểu Java hay Python nên chưa tận dụng được ngôn ngữ đúng mức
- Nhưng những đội ngũ chấp nhận triết lý và ý tưởng của Clojure sẽ có năng lực thiết kế phần mềm tốt hơn
- Cộng đồng Clojure liên tục đặt câu hỏi với cách phát triển hiện có và tìm kiếm phương pháp tốt hơn
- Các bài nói chuyện của Rich Hickey (người tạo ra Clojure) không chỉ là giới thiệu công nghệ mà còn đào sâu vào các nguyên tắc thiết kế phần mềm nền tảng
- Ở các hội nghị Clojure, trọng tâm là ý tưởng, phân tích bài báo và chia sẻ kinh nghiệm hơn là giới thiệu thư viện
Kết luận
- Clojure không chỉ là một ngôn ngữ lập trình, mà là cộng đồng của những người suy nghĩ về cách phát triển phần mềm tốt hơn
- Trong cộng đồng này, mở rộng giới hạn bản thân và khám phá những khả năng mới là giá trị cốt lõi
- Những lập trình viên trưởng thành trong nền văn hóa này không chỉ giỏi Clojure, mà còn trở thành kỹ sư phần mềm có năng lực giải quyết vấn đề tốt hơn
3 bình luận
Cá nhân tôi đang dùng Clojure và rất đồng cảm với nội dung trong bài.
Trong công việc, tôi chủ yếu dùng Python và Java(Type)Script, nhưng chỉ cần lơ là quản lý một chút là rất dễ không theo kịp sự thay đổi của chính ngôn ngữ và các thư viện, khiến mã nhanh chóng trở thành legacy code. Điều tôi rất hài lòng ở Clojure là dù nhìn lại đoạn mã đã viết sau một năm, việc chỉnh sửa và phát triển tiếp vẫn rất dễ dàng ngay lập tức.
Từ đó về sau, cho mục đích cá nhân, trừ khi bị ràng buộc bởi một thư viện cụ thể, tôi vẫn ưu tiên dùng Clojure.
Jank Jank~!
Ý kiến trên Hacker News
Nếu hỏi tôi thích kiểu lập trình nào, thì đó là xây dựng các pipeline xử lý dữ liệu trong shell và viết Clojure cùng ClojureScript trong 5 năm qua
Tôi đã dùng Clojure suốt 12 năm, và trước đó đã dùng Java hơn 12 năm
Tôi rất yêu việc viết Clojure, và khi so với các ngôn ngữ khác, nhận ra rằng mình không cần phải giải thích tình cảm sâu đậm dành cho Clojure
Đồng sáng lập của chúng tôi đặt mục tiêu tạo ra nhiều sản phẩm nhất có thể với công ty tối giản nhất
Tôi đã vận hành một doanh nghiệp SaaS bằng Clojure suốt 10 năm, và nếu không có Clojure thì có lẽ điều đó là không thể
Với những ai dùng Clojure, tôi đề xuất <a href="https://www.flow-storm.org/">Flow Storm</a>
Tôi đã học được rất nhiều từ Rich Hickey và từng rất nhiệt huyết với Clojure và FP
Có ý kiến cho rằng tài liệu trên ClojureDocs đã lỗi thời, và tôi đã muốn thêm tính năng có thể vote cho câu trả lời
Phần nói về tính ổn định của Clojure khiến tôi ngạc nhiên, vì mỗi lần thử lại hằng năm tôi đều cảm thấy mọi thứ đã thay đổi
Tôi bắt đầu với Common Lisp rồi chuyển sang Go và Rust, nhưng gần đây lại đang nhìn lại Clojure