Chuyển sang Elixir
(leemeichin.com)- Sau khi gia nhập một công ty dùng Elixir làm ngôn ngữ chính, một lập trình viên backend Ruby đã thích nghi khá nhanh dù chưa có kinh nghiệm với Elixir/Erlang, nhờ cú pháp chịu ảnh hưởng từ Ruby
- Elixir mang lại sự thú vị về mặt cú pháp và tính thân thiện với DSL giống Ruby, nhưng với mô hình hàm bất biến không có class, instance hay kế thừa, giúp giảm gánh nặng quản lý trạng thái
- Trong khi DSL của Ruby dựa vào việc định nghĩa động object và method ở runtime, DSL của Elixir dùng macro ở thời điểm biên dịch để sinh mã runtime, nhờ đó bộc lộ lỗi sớm hơn
- Cú pháp pipeline, biểu thức
with, và tuple Result dạng{:ok, result}rất hữu ích để tách luồng xử lý bình thường khỏi xử lý lỗi có thể khôi phục - So với cách trong Ruby dựa vào exception,
nil/false, hoặc trạng thái lỗi bên trong object, việc Elixir trả vềok/errormột cách tường minh mang lại cảm giác thỏa mãn hơn, và ngay cả sau 3 tháng sử dụng, trải nghiệm viết code vẫn rất thú vị
Cách một người có kinh nghiệm Ruby thích nghi với Elixir
- Công ty mới dùng Elixir làm ngôn ngữ chính cho backend, và trước khi bắt đầu, tác giả chưa từng viết mã Elixir hay Erlang
- Erlang không quá xa lạ vì tác giả từng đụng đến Prolog đôi chút, còn Elixir thì có thể học nhanh nhờ cú pháp chịu ảnh hưởng mạnh từ Ruby
- Dù chưa thật sự thành thạo các best practice, kiến trúc hay những khái niệm Erlang ở mức thấp hơn, những điều đó không ngăn cản việc bước vào Elixir
- Elixir được cảm nhận là một ngôn ngữ vẫn giữ được niềm vui lập trình mà Ruby mang lại, đồng thời giảm bớt các cạm bẫy liên quan đến trạng thái thường thấy trong các ngôn ngữ OOP
- Không có class, instance hay kế thừa
- Dựa trên tính bất biến và phong cách hàm
- Không bị ràng buộc bởi hệ thống kiểu tĩnh
- Mã có trạng thái thay đổi được khó bảo trì hơn nhiều, và không phải lúc nào cũng rõ trạng thái đó là cục bộ trong hàm, thuộc instance hay thuộc class
- Trong Ruby, mọi thứ đều là object nên mỗi object đều có thể mang trạng thái, và trong quá trình tạo ra DSL trực quan, trạng thái ở cấp hàm, instance và class rất dễ bị trộn lẫn
- Eigenclass trong Ruby là một khái niệm giống như class của class, và việc một instance sửa đổi eigenclass có thể ảnh hưởng đến trạng thái của các instance khác
Sự khác biệt giữa DSL và macro ở thời điểm biên dịch
- Elixir cũng hỗ trợ DSL và có cú pháp quen thuộc như Ruby, nhưng khác biệt cốt lõi nằm ở thời điểm DSL được tạo ra
- DSL của Elixir là macro ở thời điểm biên dịch và sinh ra mã runtime
- DSL của Ruby dựa vào việc sửa đổi runtime để định nghĩa động object và method
- Nếu dùng sai macro Elixir thì lỗi sẽ phát sinh trong library, còn nếu mã macro được sinh ra sai thì sẽ xuất hiện lỗi từ compiler
- Vì những lỗi này lộ ra ở thời điểm biên dịch, nên test có thể tập trung hơn vào logic ứng dụng thực tế
Pipeline và with tạo nên luồng xử lý
- Toán tử pipeline
|>của Elixir rất hữu ích, nhưng toán tử threading của Clojure cũng có những điểm mạnh riêng->của Clojure đưa kết quả của biểu thức trước vào đối số đầu tiên của hàm tiếp theo, giống|>của Elixir->>của Clojure đưa kết quả trước đó vào đối số cuối cùng của hàm tiếp theo- Khác biệt này tuy nhỏ nhưng hữu ích khi viết mã tương tác mà không cần dùng hàm ẩn danh
- Biểu thức
withcủa Elixir gợi nhớ đếnlettrong Haskell và Lisp, đồng thời hoạt động như thể đã tích hợp sẵnotherwise- Có thể tách một hàm phức tạp thành đường đi bình thường ở phía trên và xử lý lỗi ở phía dưới
- Nếu không pattern match được với
{:ok, result}thì có thể coi đó là lỗi có thể khôi phục, nên rất hợp với tuple Result
Ưu điểm của việc trả về lỗi một cách tường minh
- Trong Ruby, việc dùng exception cho luồng điều khiển là khá phổ biến, và thay vì kiểu lỗi tường minh, nhiều trường hợp dựa vào kết quả thành công,
nil,false, hoặc trạng thái lỗi bên trong object- Ví dụ,
model.update(params)sẽ trả vềfalsekhi thất bại và lưu lý do thất bại trongmodel.errors - Điều này lại dẫn trở về vấn đề trạng thái có thể thay đổi
- Ví dụ,
- Cách biểu đạt rõ ràng giá trị trả về là
ok/errorhoặcleft/right, giống như monadResult/Either, đem lại cảm giác thỏa mãn hơn so với luồng exception kiểu Ruby - Ngay cả sau 3 tháng dùng Elixir, với tác giả, Elixir vẫn là ngôn ngữ tiếp nối cảm giác “lập trình là điều thú vị” mà Ruby từng mang lại
1 bình luận
Các ý kiến trên Hacker News
Gần đây, trong vài tuần qua tôi gần như ngày nào cũng xem video YouTube về Elixir
Có vẻ như vừa có một hội nghị Elixir cách đây không lâu, và sau khi tôi xem vài video thì YouTube cứ tiếp tục đề xuất nội dung về Elixir
Tôi thích các ý tưởng của Erlang, nhưng trước đây khi động tới những thứ như máy chủ chat ejabberd, tôi thấy nó quá khác thường
Mỗi lần xem video của Joe Armstrong, tôi lại nghĩ ông là một thiên tài bị đánh giá thấp; các tiến trình cô lập và truyền thông điệp, tức mô hình actor, có vẻ là một ứng viên tốt cho tương lai của lập trình phân tán
Trong video về ngôn ngữ Misty mới của Crockford https://www.youtube.com/watch?v=R2idkNdKqpQ cũng thật ấn tượng khi câu hỏi lại hướng sang Elixir/Erlang
Tuy vậy, nhìn vào “phép màu” của LiveView và LiveBook thì tôi vẫn chưa bị thuyết phục 100%. Những thứ trông quá hay thường có bẫy, còn các thuật ngữ lạ như GenServer và kiến thức đặc thù của nền tảng tạo cảm giác như bước vào một cánh cổng một chiều, nên tôi không biết đó có phải là kiến thức có thể chuyển sang nơi khác không
Trong bối cảnh Go cũng có câu chuyện đồng thời khá ổn, một ngôn ngữ và nền tảng với cộng đồng nhỏ mang rủi ro lớn về tuyển dụng, thư viện và tài liệu. Dù vậy, những gì cộng đồng Elixir đạt được thật sự ấn tượng, và tôi cũng mong chờ xem việc triển khai hệ thống kiểu sẽ ra sao
sendhoạt động thế nào là đượcGenServer là sự khái quát hóa tự nhiên của một pattern mà bạn sẽ tự tay viết vô số lần, và kiến thức về actor cũng có thể mang sang nơi khác
Go cũng có các thư viện triển khai trừu tượng actor, và hộp thư của process có thể xem như một hàng đợi thông điệp giống channel của Go. Khác biệt nằm ở cách trình thông dịch và việc nhường quyền xử lý của processor hoạt động
Ngược lại, LiveView đúng là trông như phép màu. Thực tế có rất nhiều bộ phận chuyển động ăn khớp với nhau, và vì đó là kết quả của công việc đã tích lũy trong nhiều cộng đồng suốt 10 năm qua, nên dù có nhiều lớp trừu tượng, bản thân ý tưởng đã chín muồi
Những thứ như Riak đã đi trước thời đại, và gần như đã có ý tưởng rằng ta có thể xây dựng các hệ thống phân tán vững chắc theo cách tạo GenServer
Cả Chris lẫn José đều đang nỗ lực nhiều để tăng mức độ hiện diện bên ngoài giới Elixir ban đầu, và những người trong cộng đồng cũng đang làm điều tương tự ở cấp độ của riêng họ
Tuyển dụng không phức tạp như bạn nghĩ. Có khá nhiều người muốn làm việc bằng ngôn ngữ này, và các lập trình viên giỏi bắt kịp khá nhanh
Dù vậy, khía cạnh ngôn ngữ ngách vẫn cần được thuần hóa. Đó là rủi ro đối với bản thân ngôn ngữ, nhưng trong ngắn hạn cũng có thể là phần thưởng cho việc thành thạo một công nghệ ngách
Phần lớn mọi thứ bạn đều có thể nhìn vào, hiểu nó làm gì và đọc được code. JavaScript thì nhiều khi không như vậy
Đúng là tuyển dụng và tìm tutorial khó hơn. Ví dụ, tìm người đã từng làm đúng việc tôi muốn làm bằng Elixir khó hơn JavaScript rất nhiều
Thư viện không phải vấn đề lớn, và cũng khá nhiều. Khi làm việc với API, có những lúc có thư viện client cho JavaScript hoặc Python nhưng không có cho Elixir; tuy nhiên thường thì dùng API chỉ với một HTTP client cũng không quá khó
Giờ tôi dùng Node.js không phải vì muốn, mà vì phải dùng. Sau khi thử, tôi nghĩ mình sẽ tuyệt đối không bắt đầu dự án mới bằng Node.js
Các công cụ kiểm soát đồng thời, khả năng mở rộng, độ tin cậy và khả năng quan sát của Node.js tụt khá xa so với hệ sinh thái BEAM
Tôi nghĩ đôi khi người ta cảm thấy Elixir quá tốt đến mức khó tin là vì họ đã quá quen với những nền tảng không được thiết kế cho khả năng phục hồi
Với LiveBook, nếu bỏ qua các chi tiết chính xác về tính toán khác biệt hay nén dữ liệu, thì khá dễ để có một mô hình tư duy đúng. Nếu gấp, bạn nhìn vào các thông điệp WebSocket cũng có thể hiểu khá rõ chuyện gì đang xảy ra
LiveView trực quan đến đáng ngạc nhiên
Sức hút lớn của Elixir/Erlang là trong bối cảnh web server, việc chạy tác vụ nền thật sự rất dễ, và bạn không phải lo I/O chặn sẽ làm đứng toàn bộ server
Ở công ty trước, tôi phải thực hiện rất nhiều HTTP request bên trong webhook handler; khi đủ nhiều việc đó xảy ra cùng lúc, tất cả process của hệ điều hành đều bận và cả site thường chết
Những lúc đó tôi đã tha thiết nghĩ rằng nếu là Elixir thì chỉ cần bật một Task lên rồi tiếp tục là xong
Ý tưởng chia sẻ object giữa các process bất đồng bộ Tokio cho cảm giác khó hơn so với tính năng hạng nhất của Elixir. Không phải là bất khả thi hay vốn dĩ khó, nhưng có vẻ khó làm cho đúng hơn
Tôi muốn đặt tác vụ nền trên một năng lực tính toán khác với HTTP request. Vì mô hình sử dụng tài nguyên khác nhau, và tôi muốn có trạng thái hoặc hàng đợi ở phía trước để làm rõ retry, xử lý lỗi và backpressure
Tất nhiên sẽ rất tốt nếu không có sự phức tạp này và cùng một framework xử lý hết, nhưng tôi nghĩ chỉ ngôn ngữ lập trình hoặc runtime thì khó đi xa đến mức đó. Giám sát và quy trình vận hành cũng phải là một phần của lời giải
Bài nói The Soul of Erlang and Elixir của Sasa Juric cho thấy rất rõ ngôn ngữ này có thể mạnh mẽ đến mức nào
https://www.youtube.com/watch?v=JvBT4XBdoUE&t=4
Rất khuyên những ai đã nghe các câu như “hãy để nó thất bại” hay “BEAM xử lý đồng thời đúng cách” nhưng vẫn chưa hình dung được. Đây thật sự là một bài nói tuyệt vời
Từ năm 1991 tôi đã lập trình bằng Erlang hằng ngày, và vẫn định tiếp tục như vậy.
Trong thời gian đó tôi cũng đã thử Haskell, Rust, Elixir, v.v. Kiểu mạnh rất có giá trị, nhưng cuối cùng vẫn quy về việc bạn thấy vui nhất ở đâu.
Erlang là ngôn ngữ lập trình mang lại cho tôi niềm vui lớn nhất, và ít nhất tôi dự định sẽ tiếp tục hack Erlang cho đến khi nghỉ hưu sau vài năm nữa.
Tôi biết đến Elixir hơn một năm một chút, và quá trình học cũng như đưa nó vào nhiều thành phần khá là thú vị.
Niềm vui lớn nhất đến từ việc chỉ riêng BEAM/OTP đã cung cấp sẵn rất nhiều thứ.
Có lần một người trong đội tôi làm cùng nói rằng “BEAM/OTP giống k8s nhưng bỏ đi các phần phức tạp”.
Tôi đã học Elixir trong vài năm, nhưng gần đây lợi thế này phần nào bị làm loãng bởi dự án migration OpenShift, tức k8s, mà tôi thiết kế.
Nếu không có k8s thì đó hẳn đã là một đề xuất hấp dẫn hơn nhiều. Thay vào đó, chúng tôi giữ stack phát triển hiện có và đạt được những gì lẽ ra làm bằng OTP thông qua các khái niệm của k8s. Tất nhiên các đánh đổi là khác nhau.
“Elixir cho bạn một ngôn ngữ thú vị như Ruby, nhưng loại bỏ các bãi mìn liên quan đến trạng thái của ngôn ngữ hướng đối tượng. Không có class, không có instance, không có kế thừa; nó bất biến, hàm hóa và không bị trói buộc bởi hệ thống kiểu tĩnh.”
Tôi muốn điều đó, nhưng cũng muốn có kiểu. Với codebase lớn, tôi tin chắc rằng kiểu mạnh là đúng, vì nó phơi bày nhiều magic và giúp suy luận dễ hơn.
Nhờ pattern matching/unification của hàm, bạn có thể biết khá rõ dữ liệu có hình dạng gì khi nó đi qua lại. Nếu vẫn bất an thì có Dialyzer.
Dialyzer là công cụ phân tích tĩnh cho Erlang/Elixir. Nó được bao gồm trong bản phát hành Erlang chuẩn, và là viết tắt của “DIscrepancy AnaLYZer for ERlang programs”.
Nó tìm các bất nhất như lỗi kiểu, code chết, kiểm thử không cần thiết trong một module Erlang đơn lẻ hoặc một nhóm ứng dụng. Nó suy luận kiểu dựa trên các chương trình đúng; chú thích kiểu không bắt buộc, nhưng nếu có thì sẽ cung cấp cảnh báo tốt hơn.
https://fly.io/phoenix-files/adding-dialyzer-without-the-pai...
https://elixir-lang.org/blog/2023/06/22/type-system-updates-...
GenServer gần với object hơn, tương tác qua message chứ không chỉ là struct gắn kèm các hàm liên quan.
Sẽ rất tốt nếu có thể đặt một template cơ bản cho các GenServer trong dự án, như đăng ký chung, thiết lập bộ nhớ, logging, rồi chuyên biệt hóa nó cho ba biến thể tác vụ cụ thể.
Elixir lấp chỗ trống này bằng sinh mã qua macro, nhưng không hay lắm. Các GenServer của tôi đầy những lặp lại vụn vặt.
Tôi nghĩ câu “Trong Ruby, việc dùng exception cho control flow là phổ biến” đơn giản là sai.
Ví dụ sau đoạn đó là phương thức
updatecủa Rails, nhưng cách dùng trong các ví dụ và generator chuẩn của Rails là phiên bảnupdatekhông dùng exception.Khi một thư viện hoặc ứng dụng có quy mô nhất định trở nên đủ phức tạp, nó sẽ chuyển sang các lớp lỗi mang tính mô tả. Xử lý lỗi dựa trên trạng thái như ActiveRecord là rất khổ sở.
Giờ Ruby đã có pattern matching ở một mức nào đó, tôi nghĩ nó có thể thay thế exception dùng cho control flow. Thay vì ném exception, hãy trả về chính class đó rồi match trên nó.
Nhiều gem dùng exception cho control flow, và điều đó cũng phổ biến trong các app và thư viện tôi đã viết hoặc bảo trì.
Rails có nhiều phương thức
bang!ném exception khi có lỗi. Nhưng nói chung trong thực tế Ruby thường dùng exception cho control flow giống như Python.Tôi xuất thân từ F# và mới chỉ thử qua Elixir một chút, nhưng tiếc là ngôn ngữ này hơi rối
Ví dụ, chữ ký hàm chỉ thể hiện tên và số lượng tham số chứ không thể hiện kiểu, nên trong các thư viện lớn, khá khó để hình dung trong đầu cái gì sẽ được đưa vào đâu
Việc truyền sai tham số cũng thường chỉ lộ ra sau khi chạy code và debug. Với các ngôn ngữ kiểu tĩnh nghiêm ngặt như F#, những vấn đề này được bắt ngay trong lúc code, nên giảm đáng kể thời gian và công sức
Vì vậy, ngoài môi trường Erlang ra, tôi tò mò điểm hấp dẫn chính của Elixir là gì
https://elixir-lang.org/blog/2023/09/20/strong-arrows-gradua...
Hiện tại đúng là nó là ngôn ngữ động, nên bản thân ngôn ngữ không cung cấp nhiều thông tin kiểu. Nó vẫn chưa phải là ngôn ngữ kiểu tĩnh
Nhưng điều thực sự khiến tôi đánh giá Elixir cao hơn các ngôn ngữ khác tôi từng dùng chính là BEAM/OTP
Theo kinh nghiệm của tôi, nhiều tính năng mà ở các ngôn ngữ khác phải kéo thư viện vào thì ở đây đã có sẵn. Những thư viện như vậy làm tăng đáng kể độ phức tạp và đôi khi đường cong học tập cũng lớn. Các tính năng liên quan đến bất đồng bộ là một ví dụ hay
Tôi nghĩ khi dùng Elixir một thời gian và làm quen với các công cụ cùng abstraction tích hợp sẵn này, bạn sẽ hiểu được sức mạnh của cả hệ sinh thái
Điểm hấp dẫn chính có lẽ là nó chạy trên BEAM với cú pháp mang cảm giác Ruby. Tôi xem BEAM là một lợi thế khá lớn
Không thể so sánh ngang với hệ kiểu của F#, nhưng có thể dùng typespec: https://hexdocs.pm/elixir/1.15.7/typespecs.html
Ví dụ, có luồng tích hợp machine learning native với Axon, Nx, BumbleBee, thậm chí có thể biên dịch cho GPU: https://www.youtube.com/watch?v=HK38-HIK6NA
Cộng đồng thực sự đang thúc đẩy mạnh mảng này
Cách LiveView hoạt động cũng hấp dẫn. Tuy nhiên .NET có Blazor
Elixir có thể dùng trong nhiều ngữ cảnh khác nhau, như Nerves cho embedded hay scripting. Theo tôi biết F# cũng có thể dùng cho scripting
Nhìn chung, BEAM và các cấu trúc liên quan là điểm mạnh, và trình quản lý gói Hex cũng thực sự tốt. Tất nhiên .NET cũng có tooling tốt
Có thể xoay xở bằng typespec, pattern matching và guard, nhưng khá phiền, và ít nhất trong plugin JetBrains do cộng đồng phát triển thì không thân thiện với IDE/IntelliSense
Có lẽ nên viết sau khi dùng 2 năm, chứ không phải lúc mới bắt đầu
Cái mới lúc đầu luôn trông như một giải pháp vạn năng sáng bóng không tì vết
Ấn tượng ban đầu cũng có chỗ đứng riêng, và đó là thứ tôi cung cấp. Vì đối tượng là các chuyên gia Ruby, không phải chuyên gia Elixir/Erlang
Elixir khi đó cũng như bây giờ vẫn là một viên ngọc. Nó không hoàn hảo, nhưng bền bỉ một cách đáng kinh ngạc
Tôi đã dùng Ruby từ khoảng năm 2007
Mỗi lần cố chuyển sang thứ “hay ho” tiếp theo, cuối cùng tôi lại quay về Ruby, vì tôi thích Ruby, làm việc nhanh nhất với Ruby, và chỉ vậy thôi
Nhưng gần đây tôi thấy nói về Elixir nhiều hơn hẳn, và tôi thích vẻ ngoài cũng như cảm giác của nó. Có lẽ tôi nên bắt đầu một side project mới sớm để thử dùng nó