Ok, vì bài này hơi khó hiểu nên mình xin giải thích luôn. Ý đồ của bài này là như sau.
Như bạn Thích Màu Hường nói, đúng là mình nghĩ theo cách thứ hai thật. Đối với Procedural, cải tiến là việc đóng gói và tái sử dụng code theo hàm phương thức. Đối với OOP là đóng gói và tái sử dụng hàm và phương thức theo cấu trúc dữ liệu. Đối với Functional sẽ là đóng gói và tái sử dụng phương thức theo từng kiểu cấu trúc dữ liệu và từng loại chức năng khác nhau.
Ok, đến đoạn thứ hai polymorphism được chia ra thành ba loại:
- Ad-hoc polymorphism
- Sub-type polymorphism
- Parametric polymorphism
Đối với Parametric polymorphism ở trong cả ngôn ngữ OOP và FP đều có, nó chính là Generic. Ngoài Parametric, loại polymorphism được dùng chủ yếu trong OOP là Sub-type (chính là chức năng kế thừa của C# và Java). Còn polymorphism chính còn lại của các ngôn ngữ Functional như Haskell, Elixir, Rust là Ad-hoc polymorphism.
Đối với OOP Sub-type đúng như các bạn mention ở trên Poll: class A lai có class B class C kế thừa, class C lại có class D class E kế thừa. Do đó, mô hình kế thừa có cấu trúc dạng cây như sau:
Xem tệp đính kèm 234273
Đối với FP thì khác, FP chia riêng cấu trúc ra thành hai loại:
- type/struct định nghĩa kiểu dữ liệu.
- typeclass/trait/protocol/category định nghĩa chức năng và có thể chứa implementation.
Việc một type đi implement một typeclass tương ứng với việc chúng ta đã tạo ra một adapter giữa kiểu dữ liệu và chức năng của nó.
Mô hình về type system của FP có thể được mô phỏng bằng đồ thị hai phía như sau:
Xem tệp đính kèm 234276
Vậy điểm khác nhau giữa hai loại polymorphism này là gì. Mình xin trả lời là tính đóng gói.
Quay lại lịch sử của ngôn ngữ lập trình bắt đầu từ Procedural. Đối với Procedural chỉ cần có hai kiểu cấu trúc một là struct hai là hàm là đủ.
Vậy tại sao OOP phải nghĩ ra class. Đó là vì nếu để các struct và các method tách biệt một cách tùy tiện thì sẽ không thể biết được struct đó thực hiện nhiệm vụ gì. Ngoài ra nhiều hàm kết hợp với nhau mới tạo một chức năng và chức năng thường gắn với một cấu trúc dữ liệu chính. Do đó gắn các phương thức với cấu trúc dữ liệu là hợp lý nhất.
Tuy nhiên do đặc điểm của các ngôn ngữ high abstraction là nếu bên dưới class implementation sai, tính đóng gói không tốt thì bên trên dùng kiểu gì dùng cũng sai. Do đó để tránh người dùng truy cập thay đổi nhưng phương thức và thuộc tính ngoài ý muốn, OOP sử dụng hai access modifier là public và private.
Ngoài việc muốn đóng gói tốt các chức năng theo cấu trúc dữ liệu, để có thể tái sử dụng và customize các chức năng sẵn có, OOP sinh ra tính kế thừa và một access modifier nữa là protected. Tuy nhiên do việc chia sẻ chung thuộc tính và phương thức giữa các chức năng khác nhau khi kế thừa khá là nguy hiểm. Cho nên nhiều ngôn ngữ OOP như Java, C# không cho phép đa kế thừa. Thậm chí Golang còn loại bỏ kế thừa ra khỏi OOP do việc customize lại chức năng cũng có thể dẫn đến sai sót trong implementation bên dưới. Thay vào đó Golang chỉ cho phép tái sử dụng chức năng bằng cách sử dụng embedded struct. Ở trong Python cho phép sử dụng đa kế thừa nhưng không phát triển cũng vì antipattern như trên.
Sang tới FP việc đóng gói đã cải tiến hơn bởi phân tách được kiểu dữ liệu và chức năng ra thành hai cấu trúc riêng biệt. Còn việc xử lý vấn đề conflict ntn thì mình đã đề cập ở trên mình xin phép không đề cập lại. Do tính đóng gói của FP khá là tốt nên thậm chí việc implement các principle như SOLID hay Clean Architecture ở trong FP thậm chí còn tốt hơn OOP rất nhiều.
Trong bài toán thứ nhất của bạn Thích màu hường, tại sao mình biết là FP có thể cover được các bài toán của OOP. Đơn giản là vì Sub-Type polymorphism của OOP là dạng cây còn Ad-hoc Polymorphism của mình là dạng network. Tuy là dạng đồ thị hai phía tuy nhiên, do typeclass của FP có tính năng subclass tương tự như inheritence của OOP. Do đó mình có thể tự tin cover các bài toán của OOP bằng các ngôn ngữ FP.
Tính đa hình của FP có thể cover tính đa hình của OOP, vậy tính đa hình của OOP có thể cover FP hay không? Các bạn có thể thấy ở trong OOP cũng có tính năng gần gần tương tự như typeclass và ad-hoc polymorphism của FP, đó là interface. Vậy interface khác với typeclass chỗ nào. Trong bài toán thứ 2 mình viết nhằm mục đích là nêu ra điều đó.
Điểm khác nhau thứ nhất là name collision, tên các phương thức ở trong interface có thể bị name collision với nhau nhưng typeclass thì không. Một class không thể implement hai interface có cùng tên method nhưng lại khác nhau về input output, tuy nhiên typeclass có thể làm được điều này.
Điểm khác nhau thứ hai ví dụ như trong bài toán của mình bạn sẽ thấy ba interface ICar, ISubmarine, IAircraft vì có chung một phương thức là simulate. Do đó việc implement hàm simulate cho cả ba interface này trong cùng một class là vô nghĩa. Còn đối với FP đương nhiên việc này lại vô cùng có nghĩa. Ví dụ bạn có một base typeclass sử dụng chung giữa các typeclass khác nhau, sau đó một type đi implement cả ba typeclass này thì ba typeclass này vẫn có thể hoạt động độc lập mà không ảnh hưởng đến nhau. Do đó khi bạn run hàm simulate với hybrid vehicle trong OOP n lần đi nữa thì hàm simulate vẫn thể hiện một cách giống nhau, còn đương nhiên khi simulate Car hoặc simulate Aircraft hay simulate Submarine trên vehicle này trong FP thì với mỗi chức năng vehicle sẽ hoạt động một cách khác nhau. Ở đây tính polymorphism của interface trong OOP đó là với cùng một view interface có thể có nhiều kiểu dữ liệu khác nhau, tuy nhiên đối với một kiểu dữ liệu dù là với interface nào đi nữa thì biểu hiện của nó cũng phải giống nhau. Còn tính polymorphism của typeclass trong FP đó là ngoài việc một view có thể view nhiều kiểu dữ liệu thì đối với các view khác nhau, kiểu dữ liệu cũng có thể có các behavior khác nhau. Ví dụ như trong OOP có n class và m interface, mỗi interface có l hàm thì cuối cùng bạn cũng chỉ có nhiều nhất n * l phương thức khác nhau. Còn đối với FP thì bạn sẽ có tối n * m * l phương thức. Điều này khác hẳn với OOP và thể hiện được ý tưởng của Category Theory trong toán học.
Xem tệp đính kèm 234401
Vậy giờ mọi người có thể thắc mắc là việc implement n * m * l phương thức sẽ phức tạp hơn việc implement n * l phương thức hay không thì mình xin giải thích là các chức năng ở trong typeclass có thể tái sử dụng được. Do đó nếu bạn tái sử dụng một chức năng đến cấp thứ 3 thôi thì số lượng phương thức và utilities sinh ra cũng khá nhiều. Và FP cũng không bị vấn đề về name collision như trong OOP nên số lượng chức năng mà một kiểu dữ liệu có thể tải trong FP là rất là lớn - ví dụ như Rust hay Elixir số lượng ultilities cho các kiểu dữ liệu khác nhau như String, List, Set rất là nhiều, ngược lại với một số ngôn ngữ OOP như Golang rất thiếu hụt về List comprehension và các ultilities xử lý cấu trúc dữ liệu khác - Ruby và JS là một trong số những ngoại lệ. Đó là chưa tính đến việc typeclass có thể parametric, generic cho nhiều tham số về kiểu khác nhau. Do đó khi sử dụng FP mình được lợi nhiều hơn là mất.
Ok, giờ mình giải thích xong rồi. Không biết bên OOP có đưa ra bài toán nào thử thách không nhỉ hay mình sẽ đưa tiếp bài toán của mình.
cc
@Thích Màu Hường