Các trường hợp mồ côi trong Haskell


86

Khi biên dịch ứng dụng Haskell của tôi với -Walltùy chọn, GHC phàn nàn về các phiên bản mồ côi, ví dụ:

Publisher.hs:45:9:
    Warning: orphan instance: instance ToSElem Result

Lớp kiểu ToSElemkhông phải của tôi, nó được định nghĩa bởi HStringTemplate .

Bây giờ tôi biết cách khắc phục điều này (di chuyển khai báo phiên bản vào mô-đun nơi Khai báo Kết quả) và tôi biết lý do tại sao GHC muốn tránh các phiên bản mồ côi , nhưng tôi vẫn tin rằng cách của tôi tốt hơn. Tôi không quan tâm nếu trình biên dịch có bất tiện hay không - đúng hơn là tôi.

Lý do tôi muốn khai báo ToSElemcác phiên bản của mình trong mô-đun Publisher là vì nó là mô-đun Publisher phụ thuộc vào HStringTemplate, không phải các mô-đun khác. Tôi đang cố gắng duy trì sự tách biệt các mối quan tâm và tránh để mọi mô-đun phụ thuộc vào HStringTemplate.

Tôi nghĩ rằng một trong những ưu điểm của các lớp kiểu của Haskell, chẳng hạn khi so sánh với các giao diện của Java, là chúng mở chứ không phải đóng và do đó các thể hiện không phải được khai báo ở cùng một nơi với kiểu dữ liệu. Lời khuyên của GHC dường như là bỏ qua điều này.

Vì vậy, những gì tôi đang tìm kiếm là một số xác thực rằng suy nghĩ của tôi là đúng đắn và rằng tôi sẽ có lý khi bỏ qua / ngăn chặn cảnh báo này hoặc một lý lẽ thuyết phục hơn để chống lại việc làm theo cách của tôi.


Cuộc thảo luận trong các câu trả lời và nhận xét minh họa rằng có sự khác biệt lớn giữa việc xác định các cá thể mồ côi trong một tệp thực thi , khi bạn đang làm, so với trong một thư viện được hiển thị cho những người khác. Câu hỏi cực kỳ phổ biến này minh họa cho người dùng cuối của một thư viện xác định chúng có thể gây nhầm lẫn như thế nào đối với các trường hợp mồ côi.
Christian Conkle

Câu trả lời:


94

Tôi hiểu tại sao bạn muốn làm điều này, nhưng không may, nó có thể chỉ là ảo tưởng rằng các lớp Haskell dường như "mở" theo cách bạn nói. Nhiều người cảm thấy rằng khả năng làm điều này là một lỗi trong đặc tả Haskell, vì những lý do tôi sẽ giải thích bên dưới. Dù sao, nếu nó thực sự không phù hợp với trường hợp bạn cần được khai báo trong mô-đun nơi lớp được khai báo hoặc trong mô-đun nơi kiểu được khai báo, đó có thể là dấu hiệu cho thấy bạn nên sử dụng một newtypehoặc một số trình bao bọc khác xung quanh loại của bạn.

Các lý do tại sao các thể hiện mồ côi cần phải tránh sâu xa hơn sự tiện lợi của trình biên dịch. Chủ đề này khá gây tranh cãi, như bạn có thể thấy từ các câu trả lời khác. Để cân bằng cuộc thảo luận, tôi sẽ giải thích quan điểm rằng không bao giờ nên viết các trường hợp mồ côi, mà tôi nghĩ đó là ý kiến ​​đa số của các Haskellers có kinh nghiệm. Ý kiến ​​riêng của tôi là ở đâu đó ở giữa, mà tôi sẽ giải thích ở phần cuối.

Vấn đề bắt nguồn từ thực tế là khi tồn tại nhiều hơn một khai báo cá thể cho cùng một lớp và kiểu, không có cơ chế nào trong Haskell tiêu chuẩn để chỉ định cái nào sẽ sử dụng. Đúng hơn, chương trình bị trình biên dịch từ chối.

Hiệu ứng đơn giản nhất của việc đó là bạn có thể có một chương trình đang hoạt động hoàn hảo đột ngột ngừng biên dịch do một người khác thực hiện thay đổi trong một số phụ thuộc xa vào mô-đun của bạn.

Thậm chí tệ hơn, có thể một chương trình đang làm việc bắt đầu gặp sự cố trong thời gian chạy vì một sự thay đổi xa. Bạn có thể đang sử dụng một phương thức mà bạn đang giả định đến từ một khai báo phiên bản nhất định và nó có thể được thay thế một cách âm thầm bằng một phiên bản khác đủ khác để khiến chương trình của bạn bắt đầu gặp sự cố không thể giải thích được.

Những người muốn đảm bảo rằng những vấn đề này sẽ không bao giờ xảy ra với họ phải tuân theo quy tắc rằng nếu bất kỳ ai, ở bất kỳ đâu, đã từng khai báo một thể hiện của một lớp nhất định cho một kiểu nhất định, thì không có trường hợp nào khác phải được khai báo lại trong bất kỳ chương trình nào được viết bởi bất cứ ai. Tất nhiên, có một cách giải quyết là sử dụng một newtypeđể khai báo một phiên bản mới, nhưng đó luôn là một sự bất tiện nhỏ và đôi khi là một sự bất tiện lớn. Vì vậy, theo nghĩa này, những người cố tình viết các trường hợp mồ côi là khá bất lịch sự.

Vì vậy, những gì nên được làm cho vấn đề này? Trại chống cá thể mồ côi nói rằng cảnh báo GHC là lỗi, nó cần phải là lỗi từ chối mọi nỗ lực khai báo cá thể mồ côi. Trong khi chờ đợi, chúng ta phải thực hiện kỷ luật tự giác và tránh chúng bằng mọi giá.

Như bạn đã thấy, có những người không quá lo lắng về những vấn đề tiềm ẩn đó. Họ thực sự khuyến khích việc sử dụng các trường hợp mồ côi như một công cụ để tách các mối quan tâm, như bạn đề xuất, và nói rằng người ta chỉ nên đảm bảo trong từng trường hợp cụ thể rằng không có vấn đề gì. Tôi đã gặp nhiều bất tiện trước những trường hợp mồ côi của người khác để tin rằng thái độ này là quá ung dung.

Tôi nghĩ giải pháp phù hợp sẽ là thêm một phần mở rộng vào cơ chế nhập của Haskell để kiểm soát việc nhập các phiên bản. Điều đó sẽ không giải quyết được hoàn toàn các vấn đề, nhưng nó sẽ mang lại một số trợ giúp nhằm bảo vệ các chương trình của chúng tôi khỏi thiệt hại từ các cá thể mồ côi đã tồn tại trên thế giới. Và sau đó, theo thời gian, tôi có thể tin rằng trong một số trường hợp hạn chế nhất định, có lẽ một trường hợp mồ côi có thể không tệ như vậy. (Và chính sự cám dỗ đó là lý do khiến một số người trong trại chống trẻ mồ côi phản đối đề xuất của tôi.)

Kết luận của tôi từ tất cả những điều này là ít nhất vào lúc này, tôi thực sự khuyên bạn nên tránh khai báo bất kỳ trường hợp mồ côi nào, hãy quan tâm đến người khác nếu không vì lý do nào khác. Sử dụng a newtype.


4
Đặc biệt, đây ngày càng là một vấn đề với sự phát triển của các thư viện. Với hơn 2200 thư viện trên Haskell và 10 nghìn mô-đun riêng lẻ, nguy cơ chọn các bản sao sẽ tăng lên một cách thảm hại.
Don Stewart

16
Re: "Tôi nghĩ giải pháp phù hợp sẽ là thêm một phần mở rộng vào cơ chế nhập của Haskell để kiểm soát việc nhập các phiên bản" Trong trường hợp ý tưởng này khiến bất kỳ ai quan tâm, có thể nên xem xét ngôn ngữ Scala để làm ví dụ; nó có các tính năng rất giống như thế này để kiểm soát phạm vi của 'implicits', có thể được sử dụng rất giống các trường hợp typeclass.
Matt

5
Phần mềm của tôi là một ứng dụng hơn là một thư viện, vì vậy khả năng gây ra sự cố cho các nhà phát triển khác là khá nhiều. Bạn có thể coi mô-đun Nhà xuất bản là ứng dụng và phần còn lại của mô-đun như một thư viện nhưng nếu tôi phân phối thư viện thì nó sẽ không có Nhà xuất bản và do đó, các phiên bản mồ côi. Nhưng nếu tôi chuyển các phiên bản vào các mô-đun khác, thư viện sẽ gửi kèm theo một phần phụ thuộc không cần thiết vào HStringTemplate. Vì vậy, trong trường hợp này, tôi nghĩ trẻ mồ côi không sao, nhưng tôi sẽ chú ý đến lời khuyên của bạn nếu tôi gặp phải vấn đề tương tự trong một bối cảnh khác.
Dan Dyer

1
Đó nghe có vẻ là một cách tiếp cận hợp lý. Điều duy nhất cần chú ý sau đó là nếu tác giả của mô-đun bạn nhập thêm phiên bản này vào phiên bản mới hơn. Nếu phiên bản đó giống với phiên bản của bạn, bạn sẽ cần xóa phần khai báo phiên bản của riêng mình. Nếu phiên bản đó khác với phiên bản của bạn, bạn sẽ cần đặt một trình bao bọc kiểu mới xung quanh loại của bạn - đây có thể là một bản tái cấu trúc đáng kể mã của bạn.
Yitz

@Matt: thực sự, đáng ngạc nhiên là Scala có được cái này đúng chỗ Haskell không có! (ngoại trừ tất nhiên Scala thiếu cú ​​pháp hạng nhất cho loại máy móc lớp, điều này thậm chí còn tệ hơn ...)
Erik Kaplun

44

Hãy tiếp tục và ngăn chặn cảnh báo này!

Bạn đang ở trong công ty tốt. Conal làm điều đó trong "TypeCompose". "chp-mtl" và "chp-transformers" làm được, "control-monad-exception-mtl" và "control-monad-exception-monadsfd" làm được, v.v.

btw bạn có thể đã biết điều này, nhưng đối với những người không và vấp phải câu hỏi của bạn khi tìm kiếm:

{-# OPTIONS_GHC -fno-warn-orphans #-}

Biên tập:

Tôi thừa nhận những vấn đề mà Yitz đề cập trong câu trả lời của anh ấy là những vấn đề thực sự. Tuy nhiên, tôi thấy việc không sử dụng các cá thể mồ côi cũng là một vấn đề và tôi cố gắng chọn "ít tệ nạn nhất", điều này cần thận trọng khi sử dụng các cá thể mồ côi.

Tôi chỉ sử dụng một dấu chấm than trong câu trả lời ngắn của mình vì câu hỏi của bạn cho thấy rằng bạn đã nhận thức rõ về vấn đề. Nếu không, tôi sẽ ít nhiệt tình hơn :)

Có một chút chuyển hướng, nhưng điều tôi tin là giải pháp hoàn hảo trong một thế giới hoàn hảo mà không cần thỏa hiệp:

Tôi tin rằng các vấn đề mà Yitz đề cập (không biết trường hợp nào được chọn) có thể được giải quyết trong một hệ thống lập trình "toàn diện" trong đó:

  • Bạn không chỉ chỉnh sửa các tệp văn bản đơn thuần mà được hỗ trợ bởi môi trường (ví dụ: hoàn thành mã chỉ đề xuất những thứ thuộc loại có liên quan, v.v.)
  • Ngôn ngữ "cấp thấp hơn" không có hỗ trợ đặc biệt cho các lớp kiểu và thay vào đó, các bảng chức năng được chuyển một cách rõ ràng
  • Tuy nhiên, môi trường lập trình "cấp cao hơn" hiển thị mã theo cách tương tự như cách Haskell được trình bày bây giờ (bạn thường sẽ không thấy các bảng chức năng được chuyển qua) và chọn các lớp kiểu rõ ràng cho bạn khi chúng hiển nhiên (đối với ví dụ tất cả các trường hợp của Functor chỉ có một lựa chọn) và khi có một số ví dụ (danh sách nén Ứng dụng hoặc danh sách đơn nguyên Ứng dụng, Đầu tiên / Cuối cùng / nâng có thể là Monoid), nó cho phép bạn chọn trường hợp nào để sử dụng.
  • Trong mọi trường hợp, ngay cả khi phiên bản được chọn cho bạn tự động, môi trường dễ dàng cho phép bạn xem phiên bản nào đã được sử dụng, với một giao diện dễ dàng (siêu liên kết hoặc giao diện di chuột hoặc thứ gì đó)

Trở về từ thế giới tưởng tượng (hoặc hy vọng là tương lai), ngay bây giờ: Tôi khuyên bạn nên cố gắng tránh các trường hợp mồ côi trong khi vẫn sử dụng chúng khi bạn "thực sự cần"


5
Có, nhưng có thể cho rằng mỗi lần xuất hiện đó là một sai lầm của một số thứ tự. Các trường hợp xấu trong control-monad-exception-mtl và monads-fd cho Either đều xuất hiện trong tâm trí. Sẽ ít gây khó chịu hơn nếu mỗi mô-đun đó bị buộc phải xác định kiểu riêng của chúng hoặc cung cấp các trình bao bọc kiểu mới. Hầu hết mọi trường hợp mồ côi đều là một cơn đau đầu chờ đợi xảy ra và nếu không có gì khác sẽ đòi hỏi bạn phải cảnh giác thường xuyên để đảm bảo rằng nó được nhập hoặc không phù hợp.
Edward KMETT

2
Cảm ơn. Tôi nghĩ rằng tôi sẽ sử dụng chúng trong tình huống cụ thể này, nhưng nhờ có Yitz mà giờ tôi đã đánh giá tốt hơn những vấn đề mà chúng có thể gây ra.
Dan Dyer

37

Các trường hợp mồ côi là một điều phiền toái, nhưng theo tôi, chúng đôi khi là cần thiết. Tôi thường kết hợp các thư viện trong đó một loại đến từ một thư viện và một lớp đến từ một thư viện khác. Tất nhiên, tác giả của những thư viện này không thể được mong đợi để cung cấp các phiên bản cho mọi sự kết hợp có thể hình dung giữa các kiểu và lớp. Vì vậy, tôi phải cung cấp cho họ, và vì vậy họ là trẻ mồ côi.

Ý tưởng rằng bạn nên bọc loại này thành một kiểu mới khi bạn cần cung cấp một ví dụ là một ý tưởng có giá trị lý thuyết, nhưng nó quá tẻ nhạt trong nhiều trường hợp; đó là loại ý tưởng được đưa ra bởi những người không viết mã Haskell để kiếm sống. :)

Vì vậy, hãy tiếp tục và cung cấp các trường hợp mồ côi. Chúng vô hại.
Nếu bạn có thể gặp sự cố ghc với các phiên bản mồ côi thì đó là lỗi và cần được báo cáo như vậy. (Lỗi ghc đã / có về việc không phát hiện nhiều trường hợp không khó để sửa.)

Nhưng hãy lưu ý rằng một lúc nào đó trong tương lai, người khác có thể thêm phiên bản nào đó như bạn đã có và bạn có thể gặp lỗi (thời gian biên dịch).


2
Một ví dụ điển hình là (Ord k, Arbitrary k, Arbitrary v) ⇒ Arbitrary (Map k v)khi sử dụng QuickCheck.
Erik Kaplun

17

Trong trường hợp này, tôi nghĩ rằng việc sử dụng các cá thể mồ côi là ổn. Nguyên tắc chung đối với tôi là - bạn có thể xác định một phiên bản nếu bạn "sở hữu" kiểu chữ hoặc nếu bạn "sở hữu" kiểu dữ liệu (hoặc một số thành phần của nó - tức là, một phiên bản cho Có thể MyData cũng tốt, ít nhất là đôi khi). Trong những ràng buộc đó, nơi bạn quyết định đặt ví dụ là việc của riêng bạn.

Còn một ngoại lệ nữa - nếu bạn không sở hữu typeclass hoặc kiểu dữ liệu, nhưng đang tạo ra một tệp nhị phân và không phải là một thư viện, thì điều đó cũng tốt.


5

(Tôi biết mình đến bữa tiệc muộn nhưng điều này có thể vẫn hữu ích cho những người khác)

Bạn có thể giữ các cá thể mồ côi trong mô-đun của riêng họ, sau đó nếu ai đó nhập mô-đun đó thì đó là đặc biệt vì họ cần chúng và họ có thể tránh nhập chúng nếu chúng gây ra sự cố.


3

Cùng với những dòng này, tôi hiểu các thư viện WRT vị trí của trại đối tượng mồ côi, nhưng đối với các mục tiêu có thể thực thi thì các phiên bản mồ côi không ổn chứ?


3
Về mặt bất lịch sự với người khác, bạn đúng. Nhưng bạn đang tự mở ra các vấn đề tiềm ẩn trong tương lai nếu cùng một trường hợp được xác định trong tương lai ở đâu đó trong chuỗi phụ thuộc của bạn. Vì vậy, trong trường hợp này, bạn quyết định xem liệu nó có đáng để mạo hiểm hay không.
Yitz

5
Trong hầu hết các trường hợp triển khai một cá thể mồ côi trong một tệp thực thi, nó để lấp đầy một khoảng trống mà bạn mong muốn đã được xác định cho bạn. Vì vậy, nếu cá thể xuất hiện ngược dòng, lỗi biên dịch kết quả chỉ là một tín hiệu hữu ích để cho bạn biết bạn có thể xóa phần khai báo của cá thể đó.
Ben,
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.