Các đơn nguyên có phải là một thay thế khả thi (có thể thích hợp hơn) cho hệ thống phân cấp thừa kế không?


20

Tôi sẽ sử dụng một mô tả không biết ngôn ngữ của các đơn nguyên như thế này, đầu tiên mô tả các đơn sắc:

Một monoid là (đại khái) một tập hợp các hàm lấy một số loại làm tham số và trả về cùng loại.

Một đơn nguyên là (đại khái) một tập hợp các hàm lấy một loại trình bao bọc làm tham số và trả về cùng một loại trình bao bọc.

Lưu ý đó là những mô tả, không phải định nghĩa. Hãy tấn công mô tả đó!

Vì vậy, trong ngôn ngữ OO, một đơn vị cho phép các tác phẩm hoạt động như:

Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()

Lưu ý rằng đơn nguyên xác định và kiểm soát ngữ nghĩa của các hoạt động đó, chứ không phải là lớp chứa.

Theo truyền thống, trong ngôn ngữ OO, chúng tôi sẽ sử dụng hệ thống phân cấp & kế thừa lớp để cung cấp các ngữ nghĩa đó. Vì vậy, chúng tôi muốn có một Birdlớp học với phương pháp takeOff(), flyAround()land(), và vịt sẽ kế thừa những.

Nhưng sau đó chúng tôi gặp rắc rối với những con chim không biết bay, vì penguin.takeOff()thất bại. Chúng tôi phải dùng đến ngoại lệ ném và xử lý.

Ngoài ra, một khi chúng ta nói rằng Penguin là một Bird, chúng ta gặp vấn đề với nhiều kế thừa, ví dụ nếu chúng ta cũng có một hệ thống phân cấp Swimmer.

Về cơ bản, chúng tôi đang cố gắng đưa các lớp vào các danh mục (với lời xin lỗi đến các Lý thuyết Danh mục) và xác định ngữ nghĩa theo danh mục thay vì trong các lớp riêng lẻ. Nhưng các đơn vị dường như là một cơ chế rõ ràng hơn nhiều để làm điều đó hơn là hệ thống phân cấp.

Vì vậy, trong trường hợp này, chúng ta sẽ có một Flier<T>đơn nguyên như ví dụ trên:

Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()

... và chúng tôi sẽ không bao giờ khởi tạo a Flier<Penguin>. Chúng tôi thậm chí có thể sử dụng kiểu gõ tĩnh để ngăn điều đó xảy ra, có thể với giao diện đánh dấu. Hoặc kiểm tra khả năng thời gian chạy để bảo lãnh. Nhưng thực sự, một lập trình viên không bao giờ nên đặt Penguin vào Flier, theo nghĩa tương tự họ không bao giờ nên chia cho số không.

Ngoài ra, nó thường được áp dụng. Một phi công không phải là một con chim. Ví dụ Flier<Pterodactyl>, hoặc Flier<Squirrel>, không thay đổi ngữ nghĩa của các loại riêng lẻ đó.

Khi chúng tôi phân loại ngữ nghĩa theo các hàm có thể kết hợp trên một thùng chứa - thay vì theo phân cấp kiểu - nó sẽ giải quyết các vấn đề cũ với các lớp "loại làm, loại không" phù hợp với hệ thống phân cấp cụ thể. Nó cũng dễ dàng & rõ ràng cho phép nhiều ngữ nghĩa cho một lớp, Flier<Duck>cũng như Swimmer<Duck>. Có vẻ như chúng tôi đã phải vật lộn với sự không phù hợp trở kháng bằng cách phân loại hành vi với hệ thống phân cấp lớp. Monads xử lý nó một cách thanh lịch.

Vì vậy, câu hỏi của tôi là, giống như cách chúng ta đến để ủng hộ sáng tác hơn thừa kế, nó cũng có ý nghĩa để ủng hộ các đơn vị hơn thừa kế?

(BTW Tôi không chắc liệu điều này nên ở đây hay trong Comp Sci, nhưng điều này có vẻ giống như một vấn đề mô hình thực tế. Nhưng có lẽ nó tốt hơn ở đó.)


1
Không chắc tôi hiểu cách thức hoạt động của nó: một con sóc và một con vịt không bay giống nhau - vì vậy "hành động bay" cần được thực hiện trong các lớp đó ... Và người bay cần một phương pháp để tạo ra con sóc và con vịt bay ... Có lẽ trong một giao diện Flier thông thường ... Rất tiếc, đợi một chút ... Tôi có bỏ lỡ điều gì không?
assylias

Các giao diện khác với kế thừa lớp, bởi vì các giao diện xác định các khả năng trong khi kế thừa chức năng xác định hành vi thực tế. Ngay cả trong "thành phần trên kế thừa", việc xác định giao diện vẫn là một cơ chế quan trọng (ví dụ: đa hình). Các giao diện không chạy vào cùng một vấn đề thừa kế. Ngoài ra, mỗi flier có thể cung cấp các thuộc tính khả năng (thông qua giao diện & đa hình) như "getFlightSpeed ​​()" hoặc "getManuverability ()" cho container sử dụng.
Cướp

3
Bạn đang cố gắng hỏi liệu sử dụng đa hình tham số luôn luôn là một thay thế khả thi cho đa hình phụ?
ChaosPandion

yup, với sự nhăn nheo của việc thêm các chức năng tổng hợp để bảo tồn ngữ nghĩa. Các loại container được tham số hóa đã xuất hiện từ lâu, nhưng bản thân chúng không đánh tôi là một câu trả lời hoàn chỉnh. Vì vậy, đó là lý do tại sao tôi tự hỏi nếu mô hình đơn nguyên có vai trò cơ bản hơn để chơi.
Cướp

6
Tôi không hiểu mô tả của bạn về đơn sắc và đơn nguyên. Thuộc tính quan trọng của các đơn sắc là nó liên quan đến một hoạt động nhị phân kết hợp (nghĩ phép cộng dấu phẩy động, phép nhân số nguyên hoặc nối chuỗi). Một đơn nguyên là một sự trừu tượng hỗ trợ sắp xếp các tính toán khác nhau (có thể phụ thuộc) theo một số thứ tự.
Rufflewind

Câu trả lời:


15

Câu trả lời ngắn gọn là không , các đơn nguyên không phải là sự thay thế cho hệ thống phân cấp thừa kế (còn được gọi là đa hình phụ). Bạn dường như đang mô tả đa hình tham số , mà các đơn nguyên sử dụng nhưng không phải là điều duy nhất để làm như vậy.

Theo như tôi hiểu, các đơn nguyên về cơ bản không liên quan gì đến thừa kế. Tôi muốn nói rằng hai điều này ít nhiều trực giao: chúng nhằm giải quyết các vấn đề khác nhau, và vì vậy:

  1. Chúng có thể được sử dụng phối hợp trong ít nhất hai giác quan:
    • kiểm tra typeclassopedia , bao gồm nhiều lớp loại của Haskell. Bạn sẽ nhận thấy rằng có những mối quan hệ giống như thừa kế giữa chúng. Chẳng hạn, Monad có nguồn gốc từ Applicative, chính là hậu duệ của Functor.
    • các kiểu dữ liệu là các thể hiện của Monads có thể tham gia vào hệ thống phân cấp lớp. Hãy nhớ rằng, Monad giống như một giao diện - việc triển khai nó cho một kiểu nhất định cho bạn biết một số điều về kiểu dữ liệu, nhưng không phải là tất cả mọi thứ.
  2. Cố gắng sử dụng cái này để làm cái kia sẽ khó khăn và xấu xí.

Cuối cùng, mặc dù điều này là tiếp tuyến với câu hỏi của bạn, bạn có thể thích thú khi biết rằng các đơn nguyên có một số cách cực kỳ mạnh mẽ để sáng tác; đọc lên máy biến áp đơn nguyên để tìm hiểu thêm. Tuy nhiên, đây vẫn là một lĩnh vực nghiên cứu tích cực bởi vì chúng tôi (và theo chúng tôi, ý tôi là những người thông minh hơn tôi 100000x) đã không tìm ra những cách tuyệt vời để sáng tác các đơn nguyên, và có vẻ như một số đơn vị không sáng tác một cách tùy tiện.


Bây giờ để nit-pick câu hỏi của bạn (xin lỗi, tôi dự định điều này sẽ hữu ích và không làm bạn cảm thấy tồi tệ): Tôi cảm thấy có nhiều tiền đề đáng nghi ngờ mà tôi sẽ cố gắng đưa ra ánh sáng.

  1. Một đơn vị là một tập hợp các hàm lấy một loại container làm tham số và trả về cùng một loại container.

    Không, đâyMonadtrong Haskell: một loại tham số hóa m avới việc thực hiện return :: a -> m a(>>=) :: m a -> (a -> m b) -> m b, đáp ứng các luật sau:

    return a >>= k  ==  k a
    m >>= return  ==  m
    m >>= (\x -> k x >>= h)  ==  (m >>= k) >>= h
    

    Có một số trường hợp của Monad không phải là container ( (->) b) và có một số trường hợp không phải là (và không thể thực hiện) các trường hợp của Monad ( Setvì ràng buộc lớp loại). Vì vậy, trực giác "container" là một người nghèo. Xem điều này để biết thêm ví dụ.

  2. Vì vậy, trong ngôn ngữ OO, một đơn vị cho phép các tác phẩm hoạt động như:

      Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()
    

    Không hoàn toàn không. Ví dụ đó không yêu cầu Monad. Tất cả những gì nó yêu cầu là các chức năng phù hợp với các loại đầu vào và đầu ra. Đây là một cách khác để viết nó nhấn mạnh rằng đó chỉ là ứng dụng chức năng:

    Flier<Duck> m = land(flyAround(takeOff(new Flier<Duck>(duck))));
    

    Tôi tin rằng đây là một mẫu được gọi là "giao diện trôi chảy" hoặc "chuỗi phương thức" (nhưng tôi không chắc chắn).

  3. Lưu ý rằng đơn nguyên xác định và kiểm soát ngữ nghĩa của các hoạt động đó, chứ không phải là lớp chứa.

    Các kiểu dữ liệu cũng là các đơn nguyên có thể (và hầu như luôn luôn làm như vậy!) Có các hoạt động không liên quan đến các đơn nguyên. Đây là một ví dụ Haskell bao gồm ba hàm []không liên quan gì đến các đơn vị: []"định nghĩa và kiểm soát ngữ nghĩa của hoạt động" và "lớp chứa" không, nhưng điều đó không đủ để tạo ra một đơn nguyên:

    \predicate -> length . filter predicate . reverse
    
  4. Bạn đã lưu ý chính xác rằng có vấn đề với việc sử dụng hệ thống phân cấp lớp để mô hình hóa mọi thứ. Tuy nhiên, ví dụ của bạn không đưa ra bất kỳ bằng chứng nào cho thấy các đơn vị có thể:

    • Làm tốt công việc đó mà kế thừa là giỏi
    • Làm một công việc tốt ở những thứ mà thừa kế là xấu

3
Cảm ơn bạn! Rất nhiều cho tôi để xử lý. Tôi không cảm thấy tồi tệ - tôi đánh giá rất cao sự sáng suốt. Tôi sẽ cảm thấy tồi tệ hơn khi mang theo những ý tưởng tồi. :) (Đi đến toàn bộ điểm của stackexchange!)
Rob

1
@RobY Bạn được chào đón! Nhân tiện, nếu bạn chưa từng nghe về nó trước đây, tôi khuyên bạn nên LYAH vì đây là một nguồn tuyệt vời để học các đơn vị (và Haskell!) Bởi vì nó có vô số ví dụ (và tôi cảm thấy rằng việc thực hiện hàng tấn ví dụ là cách tốt nhất để giải quyết các đơn nguyên).

Có rất nhiều ở đây; Tôi không muốn tràn ngập các bình luận, nhưng một vài bình luận: # 2 land(flyAround(takeOff(new Flier<Duck>(duck))))không hoạt động (ít nhất là trong OO) vì việc xây dựng đó đòi hỏi phải đóng gói để có được thông tin chi tiết về Flier. Bằng cách xâu chuỗi các thao tác trên lớp, các chi tiết của Flier vẫn được ẩn đi và nó có thể bảo tồn ngữ nghĩa của nó. Điều đó tương tự như lý do tại Haskell, một đơn nguyên liên kết (a, M b)và không (M a, M b)vì thế mà đơn vị không phải phơi bày trạng thái của nó với chức năng "hành động".
Cướp

# 1, thật không may, tôi đang cố làm mờ đi sự khiếm khuyết nghiêm ngặt của Monad trong Haskell, bởi vì việc ánh xạ bất cứ thứ gì vào Haskell có một vấn đề lớn: thành phần chức năng, bao gồm cả thành phần trên các hàm tạo , mà bạn không thể thực hiện dễ dàng bằng ngôn ngữ dành cho người đi bộ như Java. Vì vậy, unittrở thành (hầu hết) hàm tạo trên kiểu được chứa và bindtrở thành (hầu hết) một hoạt động thời gian biên dịch ngụ ý (tức là ràng buộc sớm) liên kết các hàm "hành động" với lớp. Nếu bạn có các hàm hạng nhất hoặc một lớp Hàm <A, Monad <B >>, thì một bindphương thức có thể thực hiện ràng buộc muộn, nhưng tôi sẽ tiếp tục lạm dụng đó. ;)
Cướp

# 3 đồng ý, và đó là vẻ đẹp của nó. Nếu Flier<Thing>kiểm soát ngữ nghĩa của chuyến bay, thì nó có thể tiết lộ rất nhiều dữ liệu và hoạt động duy trì ngữ nghĩa của chuyến bay, trong khi ngữ nghĩa học "đơn nguyên" thực sự chỉ là làm cho nó có thể kết nối và đóng gói. Những mối quan tâm đó có thể không (và với những mối quan tâm tôi đã sử dụng, không phải) mối quan tâm của lớp bên trong đơn nguyên: ví dụ: Resource<String>có thuộc tính httpStatus, nhưng String thì không.
Cướp

1

Vì vậy, câu hỏi của tôi là, giống như cách chúng ta đến để ủng hộ sáng tác hơn thừa kế, nó cũng có ý nghĩa để ủng hộ các đơn vị hơn thừa kế?

Trong các ngôn ngữ không phải OO, có. Trong các ngôn ngữ OO truyền thống hơn, tôi sẽ nói không.

Vấn đề là hầu hết các ngôn ngữ không có chuyên môn về loại, nghĩa là bạn không thể thực hiện Flier<Squirrel>Flier<Bird>có các triển khai khác nhau. Bạn phải làm một cái gì đó như static Flier Flier::Create(Squirrel)(và sau đó quá tải cho từng loại). Điều đó có nghĩa là bạn phải sửa đổi loại này bất cứ khi nào bạn thêm một con vật mới và có thể sao chép khá nhiều mã để làm cho nó hoạt động.

Ồ, và trong một vài ngôn ngữ (ví dụ C #) public class Flier<T> : T {}là bất hợp pháp. Nó thậm chí sẽ không được xây dựng. Hầu hết, nếu không phải tất cả các lập trình viên OO sẽ Flier<Bird>vẫn là một Bird.


cảm ơn vì nhận xét Tôi có thêm một số suy nghĩ, nhưng chỉ là tầm thường, mặc dù Flier<Bird>là một thùng chứa được tham số hóa, không ai sẽ coi nó là một Bird(!?) List<String>Là một Danh sách không phải là Chuỗi.
Cướp

@RobY - Flierkhông chỉ là một container. Nếu bạn coi nó chỉ là một container, tại sao bạn có thể nghĩ rằng nó có thể thay thế việc sử dụng thừa kế?
Telastyn

Tôi đã mất bạn ở đó ... quan điểm của tôi là đơn nguyên là một container nâng cao. Animal / Bird / Penguinthường là một ví dụ xấu, bởi vì nó mang lại tất cả các loại ngữ nghĩa. Một ví dụ thực tế là một đơn vị REST-ish chúng tôi đang sử dụng: Resource<String>.from(uri).get() Resourcethêm ngữ nghĩa lên trên String(hoặc một số loại khác), vì vậy rõ ràng nó không phải là một String.
Cướp

@RobY - nhưng sau đó nó cũng không liên quan đến thừa kế.
Telastyn

Ngoại trừ đó là một loại ngăn chặn khác. Tôi có thể đặt String vào Resource hoặc tôi có thể trừu tượng một lớp ResourceString và sử dụng tính kế thừa. Tôi nghĩ rằng việc đặt một lớp trong một chuỗi chứa là một cách tốt hơn cho hành vi trừu tượng hơn là đưa nó vào một hệ thống phân cấp lớp với sự kế thừa. Vì vậy, "không có cách nào liên quan" theo nghĩa "thay thế / phản đối" - có.
Cướp
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.