4 điểm bởi GN⁺ 2023-08-20 | 2 bình luận | Chia sẻ qua WhatsApp
  • 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 Eitherhợ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

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])
  • Đ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ó bind là 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
    • Either là 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 Eitherbind là 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ừ
  • 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.fs tự đị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

2 bình luận

 
GN⁺ 2023-08-20
Các ý kiến trên Hacker News
  • Elixir xử lý vấn đề này khá gọn gàng bằng keyword/macro with
    Nó 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ối with xử lý việc lan truyền lỗi
    Ví dụ, nếu File.open trả về {:error, reason} thì IO.read sẽ không được chạy, và kết quả của toàn bộ with sẽ 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ốn

    • Nếu nói “trả về sớm những thứ không match”, tôi tự hỏi chuyện gì xảy ra khi tất cả các giá trị đó đều thuộc các kiểu khác nhau. Nếu là kiểu động, chẳng phải phải nhìn xem đó là kiểu gì rồi đoán nó đã thất bại ở đâu hay sao
      Cả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à else thay vì catch thì trông sẽ bớt giống xử lý ngoại lệ hơn thôi
    • Theo tôi biết, đây là dạng chịu ảnh hưởng từ where của Haskell và let của Lisp
      Có 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 else
    • Tôi thắc mắc điểm nào của cách này tốt hơn ngoại lệ. Và nếu IO.read thấ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/await của JavaScript hiện đại thì ta lại có thể dùng xử lý exception

    • Điểm đáng tiếc ở exception trong các ngôn ngữ mainstream là việc xử lý này diễn ra một cách vô hình. Không dễ nắm được tại thời điểm biên dịch rằng trong một hàm nào đó có thể phát sinh những exception nào
      Phầ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ằng Result.requireSome, Result.requireTrue, Result.mapError
      Ngượ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...
    • Tôi cho rằng cách triển khai exception truyền thống khá tệ
      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 HandleRequest cấ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
    • Tôi nghĩ Rust đã cho thấy rõ rằng Railway Oriented Programming có hỗ trợ cú pháp hạng nhất có thể gần như loại bỏ boilerplate. Cá nhân tôi cho rằng kiểu/trait Result của Rust đã làm đúng bài toán xử lý lỗi
    • Sự khác biệt giữa lỗi như một giá trị và exception hữu ích nhất khi nói về lỗi theo miền nghiệp vụ. Lỗi miền nghiệp vụ nên được biểu diễn bằng giá trị, còn các vấn đề không liên quan đến miền nghiệp vụ, chẳng hạn cơ sở dữ liệu không kết nối được, thì dùng exception tự nhiên hơn
      Ngay 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
    • Ngoài lỗi cấp hệ thống, tôi muốn biết ví dụ về trạng thái lỗi nào không phải là “lỗi dự kiến cụ thể, được định nghĩa rõ, cần xử lý tại điểm phát sinh”. Tôi muốn hiểu trong những trường hợp đó, lý do exception tốt hơn là do bản chất vấn đề hay do ràng buộc runtime
      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

    • Series xây dựng parser combinator từ đầu là một trong những tài liệu giá trị nhất mà tôi từng đọc và làm theo. Nhiều khái niệm và cơ chế trong đó cực kỳ hữu ích khi xử lý các ngôn ngữ có kiểu theo phong cách hàm
      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ệc validate thất bại có thể trả về cho caller là ổn. Nhưng nếu update-db thấ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-email thấ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 ValidationError thì báo cho người dùng, DBError thì thử lại sau 5 phút hoặc thử lại từ xa rồi thông báo, còn EmailFailed thì đưa email lại vào queue và xem như thành công sao? Có lẽ như vậy cũng ổn

    • Xử lý lỗi kiểu monad là cách lấy được ưu điểm của cả hai phía
      Ngườ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 Either thì bạn không bị ép phải kiểm tra; bạn có thể tự kiểm tra hoặc chuyển cho caller
      Ngườ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 Either thì đ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à Either rất phù hợp với sự “không biết” đó. Trong codebase Java hiện tại, khi Limit getUserLimit(User) không có kết quả, nó có thể ném exception NotFound, 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 được
      Nế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ặc orElseThrow(...). Ngoài ra, vì Either là 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 trong DbUpdater, ta có thể viết logic retry tổng quát
    • Tôi đã thử triển khai logic này theo phong cách try-catch của Java, nhưng các try lồng nhau, catch(DBError), sleep(5), retry từ xa, đưa lại vào queue cho EmailFailedError, thông báo ValidationError bị đan vào nhau và trông tệ hơn nhiều
      https://gist.github.com/Andrewp2/9d97bd213b061166d6df565ce26...
      Có thể ai đó viết được phiên bản try-catch tố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 ra
    • Xử lý lỗi ở mức thấp và mức cao có thể cùng tồn tại, nhưng tôi nghĩ phong cách này hợp hơn với ví dụ trên bảng hoặc những tình huống làm một lần rồi thôi
      Tô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ý
    • Xử lý lỗi mức thấp và mức cao có thể cùng tồn tại. Nó không khác gì việc có try/catch chuyên biệt bên trong một khối try/catch lớn
      Ví 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 caller
      Nế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
    • Có thể kết thúc như ví dụ bạn nêu, nhưng cũng có thể cấu trúc khác đi trong khi vẫn giữ cùng pipeline cấp cao cơ bản
      notify-user phụ trách thông báo cho người dùng theo từng loại ValidationErrorDBError; try-update-db gói update-db, retry sau 5 phút và retry từ xa; try-send-email có thể đưa email lại vào queue khi thất bại rồi chuyển thành thành công
      Khi đó 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 sang map/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ộ

    • Tôi tò mò liệu điều này có khác về bản chất với việc trả về dữ liệu hoặc ném ngoại lệ để nơi khác bắt hay không
    • Sẽ tốt hơn nếu có ví dụ mã để dễ hiểu
    • Cách đó cũng có nhược điểm, và tôi thấy nó đặc biệt bị giới hạn. Xử lý ngoại lệ, nhất là ngoại lệ bất đồng bộ, có thể khiến việc khôi phục lỗi trở nên khó khăn
      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ố âm
      Lúc này, kiểu lỗi tường minh như Maybe rấ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àng
      Dù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 IO khé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ào main hơ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ổi
      Kỹ 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

    • Tôi có nền tảng C# mạnh cho ứng dụng desktop, và theo thời gian phong cách viết C# của tôi ngày càng mang tính hàm hơn. Chẳng hạn dùng Linq nhiều, dùng các lớp bất biến và kiểu trả về có thể chứa thông tin lỗi chi tiết
      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
    • Trong C++ cũng dùng cách như vậy. Ta định nghĩa một kiểu trả về template có thể được tham số hoá bằng kiểu trả về T và 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ùng
  • Tô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

    • Tôi rất thích những tài liệu kiểu này. Trong các bài trình bày về Python, hồi đầu tôi từng xem bài “so you want to be a python expert” khoảng 6 tháng một lần trong vài năm, và mỗi lần xem lại hiểu thêm được nhiều hơn. Nếu bây giờ xem lại chắc tôi vẫn sẽ học được điều mới