Lập trình hướng Railway
(fsharpforfunandprofit.com)- Railway Oriented Programming là một cách tiếp cận để xử lý các nhánh thất bại lặp đi lặp lại trong ứng dụng thực tế như kiểm định, ghi log và lỗi mạng ngay bên trong pipeline hàm
- Điểm cốt lõi là giải thích giới hạn của các ví dụ chỉ nối các đường đi thành công bằng ẩn dụ đường sắt, giúp cả người dùng F# cũng dễ hình dung luồng lỗi theo trực quan
- Từ góc nhìn Haskell, nó gần với kiểu
Eithervà hợp thành Kleisli, nhưng tập trung vào các công thức xử lý lỗi và mẫu triển khai hơn là lý thuyết trừu tượng - Để áp dụng thực tế, tài liệu đi kèm thư viện F# Chessie cho NuGet, ví dụ GitHub so sánh C#·F#, và ví dụ áp dụng cho FizzBuzz
- Đây là một cách làm hữu ích nhưng không nên lạm dụng cho mọi bài toán; F# cũng không có type class nên thư viện ví dụ tự định nghĩa trực tiếp các hàm cần thiết
Tài liệu trình bày và mã ví dụ
- Trang Railway Oriented Programming là nơi tập hợp slide và mã nguồn của bài trình bày vào một chỗ
- Tài liệu bắt đầu từ vấn đề rằng chỉ với “happy path” như trong nhiều ví dụ lập trình hàm thì khó có thể xây dựng ứng dụng thực tế
- Một ứng dụng vững chắc phải đồng thời xử lý kiểm định, ghi log, lỗi mạng, lỗi dịch vụ và các tình huống ngoại lệ khác
- Mục tiêu là giới thiệu ngắn gọn, dễ hiểu cách xử lý các lỗi như vậy theo phong cách hàm sạch sẽ bằng ẩn dụ đường sắt
- Chủ đề tương tự cũng được tiếp nối trong loạt bài recipe for a functional app
- Mã thực tế có thể xem trong dự án GitHub so sánh cách tiếp cận ROP giữa C# và F#
- Dù hữu ích cho xử lý lỗi, tài liệu cũng cảnh báo không nên áp dụng cực đoan và gợi ý tham khảo thêm “Against Railway-Oriented Programming”
Video và slide
- Chủ đề này đã được trình bày tại NDC London 2014
- Cũng có các video khác của cùng bài trình bày
- Có slide cho buổi trình bày ngày 14 tháng 3 năm 2014 tại Functional Programming eXchange
- Slide PowerPoint cũng có thể tải từ GitHub và được ghi rõ là có thể tự do mượn dùng
Monad Either và hợp thành Kleisli
- Với người dùng Haskell, cách tiếp cận này có thể trông giống kiểu
Either - Ở đây nó được chuyên biệt hóa dưới dạng dùng danh sách kiểu lỗi tùy biến cho trường hợp
Left- Kiểu ví dụ là
type TwoTrack a b = Either [a] (b,[a])
- Kiểu ví dụ là
- Điều mới không phải là bản thân khái niệm xử lý lỗi, mà gần hơn là ẩn dụ đường sắt dùng để giải thích nó
- Lý do không đặt thuật ngữ Haskell chuẩn lên trước là rất rõ ràng
- Tài liệu này không phải hướng dẫn về monad, mà tập trung vào giải quyết bài toán xử lý lỗi
- Nhiều người mới tiếp cận F# chưa quen với monad, nên cách giải thích trực quan và bớt đáng sợ có thể dễ nắm bắt hơn
- Gọi ngay một kiểu two-track có
bindlà monad thì không chính xác, và tài liệu cũng không đi vào các luật monad Eitherlà công cụ quá tổng quát, nên ở đây mục tiêu là cung cấp công thức hơn là bản thân công cụ
Các thành phần của công thức xử lý lỗi
- Cách tiếp cận này là một gói kỹ thuật toàn diện hơn nhiều so với việc chỉ “dùng
Eithervàbindlà xong” - Các kỹ thuật bao gồm
- Dùng danh sách kiểu lỗi tùy biến ở cả hai phía trái và phải, thay vì dạng đơn giản như
Either String a - Tích hợp các hàm monad vào pipeline bằng
bind(>>=) - Hợp thành các hàm monad bằng hợp thành Kleisli (
>=>) - Tích hợp các hàm không phải monad vào pipeline bằng
map(fmap) - Vì F# không dùng IO monad, tích hợp các hàm unit vào pipeline bằng
tee - Ánh xạ ngoại lệ sang các trường hợp lỗi
- Kết hợp song song các hàm monad bằng
&&&trong những tình huống như kiểm định - Tận dụng lợi ích của kiểu lỗi tùy biến trong thiết kế hướng miền
- Các mở rộng như ghi log, sự kiện miền và giao dịch bù trừ
- Dùng danh sách kiểu lỗi tùy biến ở cả hai phía trái và phải, thay vì dạng đơn giản như
- Mục tiêu là tạo ra một mẫu đủ đa dụng để dùng trong gần như mọi tình huống, nhưng cũng đủ ràng buộc để ép một phong cách nhất quán
- Khi cách viết mã gần như hội tụ về một kiểu duy nhất, người bảo trì sau này sẽ dễ hiểu nhanh cấu trúc mã hơn
- Đây không phải lời giải duy nhất, nhưng có thể là một điểm khởi đầu tốt
- Ngay cả trong cộng đồng Haskell, các cách xử lý lỗi cũng không thống nhất nên có thể gây rối cho người mới
Cách áp dụng vào mã của bạn
- Nếu cần thư viện F# có sẵn, có thể xem Chessie project dùng cùng NuGet
- Một dịch vụ web mẫu áp dụng kỹ thuật này được cung cấp qua dự án GitHub
- Ví dụ áp dụng cách tiếp cận ROP cho FizzBuzz có thể xem tại Railway Oriented Programming: carbonated
- F# không có type class nên không có cách tổng quát để tái sử dụng monad
- FSharpX library cung cấp một cách tiếp cận hữu ích, nhưng thư viện
Rop.fstự định nghĩa mọi hàm từ đầu - Việc tách biệt như vậy có ưu điểm là hoàn toàn không có phụ thuộc bên ngoài
Đọc thêm
- Để định nghĩa monad đúng cách, chỉ triển khai
bindvàreturnthôi là chưa đủ - Monad là một cấu trúc đại số phải thỏa mãn các luật monad, và các luật này có liên hệ với luật monoid trong một số tình huống nhất định
- Nếu quan tâm đến
Eithervà hợp thành Kleisli, có thể xem các tài liệu sau- Monads in general
- The
Eithermonad - Kleisli categories and composition
- Comprehensive error handling approaches
2 bình luận
Railway-Oriented Programming để tạo ra các chương trình không có lỗi
Các ý kiến trên Hacker News
Elixir xử lý vấn đề này khá gọn gàng bằng keyword/macro
withNó chạy các hàm từ trên xuống dưới như
with {:ok, file_handle} <- File.open(filename), ... do {:ok, parsed} end, và nếu mỗi giá trị trả về không khớp với pattern bên trái thì sẽ trả về sớm bằng chính giá trị đóVì vậy, thay vì phải làm cho mỗi hàm nhận cả
{:ok, value}lẫn{:error, reason}, có thể để hàm chỉ nhận giá trị mà nó quan tâm, còn pattern matching trong khốiwithxử lý việc lan truyền lỗiVí dụ, nếu
File.opentrả về{:error, reason}thìIO.readsẽ không được chạy, và kết quả của toàn bộwithsẽ là{:error, reason}. Rốt cuộc, bạn chỉ cần viết luồng thành công, còn luồng thất bại có thể để caller match nếu muốnCảm giác như đang phát minh lại một nửa cơ chế xử lý ngoại lệ; chẳng qua nếu đặt tên là
elsethay vìcatchthì trông sẽ bớt giống xử lý ngoại lệ hơn thôiwherecủa Haskell vàletcủa LispCó thể xem như thiết lập các tiền điều kiện của hàm, và trong Elixir còn có thể dùng thêm cả khối
elseIO.readthất bại thì ai sẽ đóng file handle cũng là một vấn đềVài năm sau, tác giả cũng viết một bài tiếp nối tên là Against Railway Oriented Programming: https://fsharpforfunandprofit.com/posts/against-railway-orie...
Railway Oriented Programming là một ý tưởng thú vị và cũng có chỗ dùng, nhưng theo tôi nó cần một cảnh báo thật lớn. Trên thực tế, tôi thường thấy nó được dùng để tái phát minh xử lý ngoại lệ theo cách tệ hơn; nếu hiểu và dùng exception đúng cách thì có thể xử lý hầu hết các điều kiện lỗi gọn gàng và hiệu quả hơn
Ưu điểm của exception là trong đa số trường hợp, lựa chọn an toàn là mặc định. Điều kiện lỗi là tín hiệu rằng mã không thể làm đúng theo đặc tả, và nếu tiếp tục trong trạng thái đó thì dữ liệu có thể bị hỏng do các giả định sai. Exception mặc định sẽ dọn dẹp rồi truyền lên caller, trong khi các cách như Railway Oriented Programming đòi hỏi rất nhiều boilerplate dễ quên và dễ sai
Tuy vậy nó hữu ích trong hai trường hợp. Một là khi cần xử lý tại điểm phát sinh những lỗi dự kiến cụ thể, được định nghĩa rõ, chẳng hạn như validation; hai là trong môi trường không thể dùng xử lý exception. Mã bất đồng bộ dựa trên Promise của jQuery ngày trước gần như là một triển khai Railway Oriented Programming, nhưng với
async/awaitcủa JavaScript hiện đại thì ta lại có thể dùng xử lý exceptionPhần thưởng lớn của Railway Oriented Programming là chỉ cần nhìn chữ ký hàm là biết ngay có thể phát sinh những lỗi nào
Boilerplate có thể được giảm bớt tùy theo ngôn ngữ. Haskell có ký pháp
do, còn trong F# có thể viết rất gọn bằng result computation expression. Ví dụ có thể định nghĩa kiểu nhưLoginError = InvalidUser | InvalidPwd | Unauthorized of AuthError, rồi cấu thành luồng bằngResult.requireSome,Result.requireTrue,Result.mapErrorNgược lại, tôi cũng tò mò liệu có thể giảm nhẹ nhược điểm của exception không. Tôi muốn biết có linter hoặc công cụ phân tích tĩnh nào tích hợp với Java IDE, có thể tự động hiển thị các exception chưa được bắt có thể bị ném ra ở một dòng mã cụ thể hay không
https://demystifyfp.gitbook.io/fstoolkit-errorhandling/fstoo...
Trước hết, nhiều ngôn ngữ ép phải dùng các scope gượng gạo và không cần thiết để bắt exception. Chẳng hạn khi muốn khai báo và khởi tạo một biến bằng kết quả của một hàm có thể ném exception, nhưng nếu thất bại thì muốn gán một giá trị khác, ta lại phải tách phần khai báo và khởi tạo
Vấn đề lớn hơn là các điều kiện lỗi nên được xử lý ở mức thấp nhất vẫn còn đủ ngữ cảnh để xử lý đúng, trong khi mặc định của exception lại là “ném lên tận trên cùng”. Chỉ cần thêm một file cache, hàm
HandleRequestcấp trên bỗng có thể ném exception I/O; abstraction rất dễ bị rò rỉ theo kiểu đóTôi cho rằng mọi thứ một hàm có thể trả về đều phải là một phần rõ ràng của chữ ký hàm, và caller phải xử lý rõ ràng hoặc ít nhất đánh dấu rằng nó chuyển tiếp lên trên. Ngôn ngữ không nhất thiết phải đòi hỏi nhiều boilerplate cho việc này
Resultcủa Rust đã làm đúng bài toán xử lý lỗiNgay cả khi không có file hay record, cũng tùy vào việc điều đó có được kỳ vọng hay không. Nếu đó là yêu cầu tra cứu bằng ID do người dùng cung cấp, bản thân việc tra cứu ấy là validation đầu vào của người dùng. Nhưng nếu tra cứu storage bằng tên blob được lưu trong record cơ sở dữ liệu mà blob không tồn tại, thì đó là tình huống phù hợp để xử lý bằng exception
Ranh giới giữa lỗi và exception khá mờ và thay đổi theo đội ngũ cũng như miền vấn đề, nhưng tôi thấy đây là một quy tắc kinh nghiệm khá ổn
Trong mã C++, có thể kiểm tra dữ liệu từ sớm; nếu kiểm tra thất bại thì cho ứng dụng crash, rồi dùng trạng thái lỗi đã phát hiện để debug và sửa mã kiểm tra ban đầu
Vì dữ liệu đang xử lý là dữ liệu tương đối rõ ràng, tuân theo định dạng dữ liệu CAD hoặc topology hình học đã biết, nên việc xử lý điều kiện lỗi “khá dễ” ở chỗ có thể hiểu trước thế nào là dữ liệu đúng
Thành thật mà nói, toàn bộ trang này là một kho báu. Ngay cả khi không có ý định dùng ngôn ngữ hàm, nó vẫn cho ta một góc nhìn khác, và cá nhân tôi thấy điều đó rất hữu ích. Tôi cũng khuyên đọc các bài khác
Dù vậy, có lẽ cuối cùng tôi vẫn sẽ không biết monad là gì
https://fsharpforfunandprofit.com/series/understanding-parse...
Tôi băn khoăn liệu lập luận “đẩy xử lý lỗi ra xa điểm gọi là điều tệ” có đứng vững không. Vì caller là nơi có thể xử lý lỗi tốt nhất, chẳng phải nên xử lý trực tiếp thay vì chuyển xuống dưới sao?
Ví dụ, trong luồng
validate and-then update-db and-then send-email, việcvalidatethất bại có thể trả về cho caller là ổn. Nhưng nếuupdate-dbthất bại thì phải quyết định có thử lại, dùng dịch vụ khác, đưa lại vào queue, hay thông báo cho người dùng.send-emailthất bại cũng tương tựRốt cuộc chẳng phải nó sẽ thành một cấu trúc rẽ nhánh phức tạp hơn, kiểu
ValidationErrorthì báo cho người dùng,DBErrorthì thử lại sau 5 phút hoặc thử lại từ xa rồi thông báo, cònEmailFailedthì đưa email lại vào queue và xem như thành công sao? Có lẽ như vậy cũng ổnNgười ghét Go phàn nàn rằng phải kiểm tra điều kiện lỗi quá thường xuyên, và checked exception của Java cũng tương tự. Dùng
Eitherthì bạn không bị ép phải kiểm tra; bạn có thể tự kiểm tra hoặc chuyển cho callerNgười ghét unchecked exception của Java phàn nàn rằng không biết cái gì sẽ bị ném ra và khi nào. Dùng
Eitherthì điều đó trở nên tường minhĐiểm cốt lõi là khi lỗi xảy ra ta không biết phải làm gì, và
Eitherrất phù hợp với sự “không biết” đó. Trong codebase Java hiện tại, khiLimit getUserLimit(User)không có kết quả, nó có thể ném exceptionNotFound, trả vềnull, hoặc trả về giá trị mặc định; trước khi đào sâu vào code thì không thể biết đượcNếu là
Either, caller có thể tin tưởng mà không cần đọc code bên trong, và có thể dễ dàng đổi sang điều kiện thất bại khác bằng các công cụ có sẵn nhưorElse(null)hoặcorElseThrow(...). Ngoài ra, vìEitherlà biểu thức nên rất dễ trừu tượng hóa; thay vì viết mã retry chuyên biệt bên trongDbUpdater, ta có thể viết logic retry tổng quáttry-catchcủa Java, nhưng cáctrylồng nhau,catch(DBError),sleep(5), retry từ xa, đưa lại vào queue choEmailFailedError, thông báoValidationErrorbị đan vào nhau và trông tệ hơn nhiềuhttps://gist.github.com/Andrewp2/9d97bd213b061166d6df565ce26...
Có thể ai đó viết được phiên bản
try-catchtốt hơn, nhưng tôi thấy nó kém hơn hẳn so với phiên bản railway-style được đưa raTôi thích DDD, nhưng một lập trình viên bình thường như tôi cần một chu kỳ bảo trì hợp lý có thể quản lý được. Điều khiến TDD tốt trong code có thể thay đổi là nó giảm tải nhận thức khi bảo trì
Railway-oriented programming có thể mang theo rất nhiều ngữ cảnh qua nhiều phép biến đổi, và tôi muốn tránh tình huống ở cuối đường ray lại nhận một đoàn tàu chất đầy ngữ cảnh nghiệp vụ và hệ thống chưa được chuẩn bị sẵn
Tôi nghĩ DDD và onion architecture mang lại ưu điểm của cả hai bên. Cuối cùng vẫn sẽ có đường ray, nhưng nó nằm bên ngoài chứ không cắm sâu bên trong các micro app. Các vấn đề kiểu railway nên được code một cách tường minh và hơi rườm rà; nếu làm tốt, phần lớn quyết định kiểu railway là quyết định nghiệp vụ, không nên né tránh bằng một cấu trúc code thông minh
Lập trình kiểu monad rất tuyệt, nhưng có sự cám dỗ dùng nó để né các quyết định nghiệp vụ cần thiết. Nếu đưa sự mơ hồ vào type system, sau này có thể dẫn tới những tình huống khó hoặc không thể xử lý
try/catchchuyên biệt bên trong một khốitry/catchlớnVí dụ, bên trong hàm
send-email, bạn có thể chọn xử lý các lỗi tạm thời rất nhanh. Nếu kết nối được trong khoảng thời gian cho phép thì quay lại đường thành công; nếu không thì trả về cho callerNếu có một quy tắc chắc chắn, thì đó là mọi xử lý cần sự can thiệp của con người nhất thiết phải được bọc lại và chuyển lên trên. Dù có quay lại đường thành công thì cũng nên để lại log cảnh báo
notify-userphụ trách thông báo cho người dùng theo từng loạiValidationErrorvàDBError;try-update-dbgóiupdate-db, retry sau 5 phút và retry từ xa;try-send-emailcó thể đưa email lại vào queue khi thất bại rồi chuyển thành thành côngKhi đó tầng trên cùng vẫn giữ dạng
validate and-then try-update-db and-then try-send-email or-then notify-user. Có vẻ nhưand/or/thenđã được chuyển khá khéo sangmap/bindĐây là một trong những trang dạy lập trình tốt nhất tôi từng biết, vì nó dạy các khái niệm thực tế và thực dụng. Tôi đặc biệt thích khái niệm làm cho các trạng thái sai không thể biểu diễn được, và cố áp dụng nó bất kể ngôn ngữ. Tất nhiên, một số ngôn ngữ giúp việc đó dễ hơn
https://fsharpforfunandprofit.com/posts/designing-with-types...
Dùng luồng dữ liệu thay vì gọi/trả về sẽ giải quyết vấn đề này tốt hơn, và phần lớn vấn đề tự biến mất
Với mô hình gọi/trả về, vì phải trả về một thứ gì đó, nếu do lỗi mà không có giá trị để trả về thì phải trả về lỗi, hoặc như Go, trả về cả lỗi lẫn giá trị trả về bình thường. Điều này làm ô nhiễm luồng thành công. Việc xâu chuỗi phần xử lý còn lại qua một container đa hình có cải thiện đôi chút, nhưng vấn đề vẫn còn
Với luồng dữ liệu, chỉ cần không gửi gì sang bước lọc tiếp theo, nên luồng thành công hoàn toàn không bị ảnh hưởng. Lỗi có thể được gửi tới nơi như đầu ra lỗi chuẩn và được tập trung hóa ở cấp ứng dụng
Nghe có thể quá hay, nhưng chúng tôi đã thực sự dùng cách này ở Wunderlist và nó hoạt động tốt. Cùng kỹ thuật dùng khi không có giá trị cũng áp dụng nguyên xi cho trường hợp chưa có giá trị, tức xử lý bất đồng bộ
Với một luồng dữ liệu đơn giản, nếu có lỗi thì không có dữ liệu sang bước tiếp theo nên dừng lại là ổn. Nhưng nếu tại
sqrt(-1)mà lập tức “crash” rồi nhảy sang xuất lỗi hoặc ghi log, thì trong một số trường hợp sử dụng cụ thể, sẽ khó khi bạn muốn mở rộng miền để trả về một giá trị khác cho số âmLúc này, kiểu lỗi tường minh như
Mayberất hữu ích. Luồng thành công được viết như thể không có lỗi, nhưng khi hợp thành hàm thì dùng một toán tử khác. Nếu muốn khôi phục từ lỗi, bạn xử lý cả hai nhánh ngay tại chỗ đó, và sau đó có thể không cho phép lỗi nữa. Điểm đặc biệt hay là để lấy giá trị thực từ một “giá trị có thể tồn tại” trừu tượng, bạn buộc phải xử lý cả hai nhánh một cách gọn gàngDùng kiểu lỗi tường minh cũng giúp thoát khỏi lưỡng phân thành công/lỗi. Có thể biểu diễn cả các trạng thái như “không có giá trị nhưng”, “có giá trị nhưng”, và cũng có thể tích lũy lỗi từ nhiều tác vụ có thể xử lý song song rồi gom thành kết quả. Xa hơn nữa, có thể mã hóa tính phi quyết định để dùng cho những việc như duyệt đồ thị
Cũng có thể truyền trạng thái theo cách thuần túy như các lập trình viên Haskell thường làm, và monad
IOkhét tiếng của Haskell không hẳn biểu diễn một luồng thực tế, mà giống một cơ chế giới hạn các điểm tiếp xúc với thế giới bên ngoài vàomainhơn. Điều này là vì tính thuần túy, nhưng tôi cho rằng tính thuần túy không phải lúc nào cũng là giá trị tuyệt đối cần theo đuổiKỹ thuật này đa dạng hơn đường ray đơn giản rất nhiều, và cung cấp một cách gọn gàng để chỉ viết luồng thành công trong khi vẫn giữ được tính thuần túy. Nó cũng có lợi cho phân tích tĩnh dựa trên phương trình và unit test không cần mock. Tôi mong nhiều ngôn ngữ phổ biến hơn cung cấp tốt hơn hộp công cụ monad kiểu này
Đây là các liên kết liên quan: Railway Oriented Programming - https://news.ycombinator.com/item?id=31404643 - tháng 5/2022, Railway-Oriented Programming (2015) - https://news.ycombinator.com/item?id=17337155 - tháng 6/2018, Railway oriented programming - https://news.ycombinator.com/item?id=11955917 - tháng 6/2016, Railway Oriented Programming - https://news.ycombinator.com/item?id=9166943 - tháng 3/2015, Railway-oriented programming - https://news.ycombinator.com/item?id=7887134 - tháng 6/2014
Ngoài ra còn có What is railway oriented programming? (2020) - https://news.ycombinator.com/item?id=34245639 - tháng 1/2023, Railway Oriented Programming in Elixir (2015) - https://news.ycombinator.com/item?id=11958578 - tháng 6/2016
Phong cách này có tính lây lan như các monad khác, nên toàn bộ logic nghiệp vụ sẽ mang hình dạng như vậy. Nếu bạn thấy ổn với điều đó thì cứ đọc tiếp
Ngoại lệ, đúng như tên gọi, là lỗi ngoài dự kiến; chúng tua ngược stack tới nơi bắt lỗi, còn việc dọn dẹp cần thiết được xử lý tự động. Chúng phù hợp với các lỗi phát sinh ngoài phạm vi của hàm nghiệp vụ, như lỗi I/O hoặc thiếu bộ nhớ
Nếu muốn xác thực dữ liệu, bạn không cần dựa vào trình xử lý ngoại lệ hay monad. Chỉ cần biến kết quả của pipeline hàm thành dữ liệu. Nếu có lỗi xác thực thì gom chúng thành một tập hợp, rồi kiểm tra sau khi pipeline kết thúc. Thường thì nhiều khả năng bạn không muốn thất bại ngay ở lỗi đầu tiên
Nếu cần hiệu ứng phụ trong pipeline thì đừng làm vậy; hãy mô tả hiệu ứng đó bằng dữ liệu rồi thực thi sau. Nếu cần thoát sớm ở giữa chừng, hãy chia pipeline tại điểm đó, xử lý kết quả, rồi đưa vào một pipeline khác
Hoặc có thể dùng chuỗi interceptor để quyết định có tiếp tục luồng thành công hay không. Nếu ngôn ngữ hỗ trợ thì dùng pattern matching. Điều quan trọng là quyết định có bỏ qua một phần pipeline hay không được đưa ra bên ngoài hàm nghiệp vụ; khi đó hàm nghiệp vụ chỉ tập trung vào biến đổi dữ liệu thuần túy, nên khả năng tái sử dụng sẽ cao hơn
Chỉ riêng việc một chủ đề như thế này được đưa lên lại và thảo luận đã là điều tốt rồi. Tôi đã đọc trang này suốt 1 năm và làm theo các ví dụ; đây từng là một trong những tài liệu F# hay nhất
Chỉ với các khái niệm được trình bày trên trang này thôi cũng đã giúp ích cho việc tư duy theo hướng hàm trong mọi ngôn ngữ
Đây là một trang cũ, nên nếu giờ nó được quan tâm thì tôi tự hỏi liệu có phải F# đang có đà phát triển hay không. Và tôi cũng tò mò liệu lập trình viên các ngôn ngữ khác có dùng kiểu xử lý lỗi “đường ray” như Rust không
Dù vậy, lý do tôi chưa chuyển sang F# là vì phần lớn thời gian tôi dành cho việc tinh chỉnh frontend desktop. Trong lĩnh vực này, F# dường như không mang lại nhiều lợi thế. Frontend desktop sẵn sàng cho production phần lớn là mã hướng đối tượng dựa trên C#, chứ không phải F# native, rồi được nối với F# bằng một lớp glue code mỏng
Chỉ cần cần tuỳ biến UI hơi phức tạp một chút là lại phải quay về thế giới hướng đối tượng, và nếu vậy thì tôi nghĩ làm thẳng trong C# sẽ tốt hơn. Cá nhân tôi cho rằng để F# có đà phát triển thì cần có framework UI native
Tvà một enum cụ thể mô tả điều kiện kết quảNếu cần gấp thì cũng có thể dùng
std::pair, hoặc trả về câu lỗi trong một chuỗi rồi thoát sớm nếu chuỗi đó không rỗng. Nếu mọi hàm đều trả về kiểu như vậy thì câu lỗi có thể được bubble up lên tận tầng trên cùngTôi thích việc bài này cứ vài năm lại được đưa lên lại. Đây thực sự là một bài viết kiêm bài trình bày xuất sắc
Điều thú vị là mỗi lần đọc lại, tôi lại đang ở một vị trí khác trong hành trình kỹ thuật của mình, cả về năng lực lẫn triết lý, nên lần nào cũng thấy nó khác đi