Trình kiểm tra kiểu đang cho phép thay thế kiểu rất sai và chương trình vẫn biên dịch


99

Trong khi cố gắng gỡ lỗi một vấn đề trong chương trình của tôi (2 vòng tròn có bán kính bằng nhau đang được vẽ thành các kích thước khác nhau bằng Gloss *), tôi tình cờ gặp một tình huống kỳ lạ. Trong tệp xử lý các đối tượng của tôi, tôi có định nghĩa sau cho một Player:

type Coord = (Float,Float)
data Obj =  Player  { oPos :: Coord, oDims :: Coord }

và trong tệp chính của tôi, nhập Objects.hs, tôi có định nghĩa sau:

startPlayer :: Obj
startPlayer = Player (0,0) 10

Điều này đã xảy ra do tôi thêm và thay đổi các trường cho trình phát và quên cập nhật startPlayersau (kích thước của nó được xác định bởi một số duy nhất để đại diện cho bán kính, nhưng tôi đã thay đổi nó thành a Coordđể đại diện (chiều rộng, chiều cao); trong trường hợp tôi từng thực hiện người chơi phản đối một không phải vòng tròn).

Điều đáng kinh ngạc là đoạn mã trên biên dịch và chạy, mặc dù trường thứ hai không đúng loại.

Đầu tiên tôi nghĩ rằng có thể tôi đã mở các phiên bản tệp khác nhau, nhưng bất kỳ thay đổi nào đối với bất kỳ tệp nào đều được phản ánh trong chương trình đã biên dịch.

Tiếp theo, tôi nghĩ rằng có lẽ startPlayerkhông được sử dụng vì lý do nào đó. Nhận xét ra gây ra startPlayerlỗi trình biên dịch, và thậm chí lạ, thay đổi nội dung 10trong startPlayergây ra một phản hồi thích hợp (thay đổi kích thước bắt đầu của Player); một lần nữa, mặc dù nó không đúng loại. Để đảm bảo rằng nó đang đọc định nghĩa dữ liệu một cách chính xác, tôi đã chèn một lỗi đánh máy vào tệp và nó đã gây ra lỗi cho tôi; vì vậy tôi đang xem tệp chính xác.

Tôi cố gắng dán 2 đoạn mã trên vào tập tin riêng của họ, và nó nhổ ra lỗi mong rằng trường thứ hai của Playertrong startPlayerlà không chính xác.

Điều gì có thể cho phép điều này xảy ra? Bạn sẽ nghĩ rằng đây là điều mà trình kiểm tra loại của Haskell nên ngăn chặn.


* Câu trả lời cho vấn đề ban đầu của tôi, hai hình tròn có bán kính được cho là bằng nhau được vẽ theo các kích thước khác nhau, là một trong các bán kính thực sự là âm.


26
Như @Cubic đã lưu ý, bạn chắc chắn nên báo cáo vấn đề này cho những người bảo trì Gloss. Câu hỏi của bạn minh họa một cách độc đáo cách phiên bản mồ côi không đúng cách của thư viện đã làm rối mã của bạn .
Christian Conkle

1
Làm xong. Có thể loại trừ các trường hợp không? Họ có thể yêu cầu nó để thư viện hoạt động, nhưng tôi không cần. Tôi cũng nhận thấy rằng họ đã định nghĩa Màu Num. Nó chỉ là vấn đề thời gian trước khi điều đó làm tôi khó chịu.
Chất gây ung thư

@Cubic Chà, quá muộn. Và tôi chỉ tải xuống cách đây một tuần hoặc lâu hơn bằng cách sử dụng Cabal được cập nhật, cập nhật; vì vậy nó phải là hiện tại.
Chất gây ung thư

2
@ChristianConkle Có khả năng tác giả của gloss đã không hiểu TypeSynonymInstances làm gì. Trong mọi trường hợp, điều này thực sự cần phải đi xa (hoặc thực hiện Pointmột newtypehoặc sử dụng tên nhà điều hành khác ala linear)
Cubic

1
@Cubic: Bản thân TypeSynonymInstances không phải là xấu (mặc dù không hoàn toàn vô hại), nhưng khi bạn kết hợp nó với OverlappingInstances, mọi thứ sẽ rất thú vị.
John L

Câu trả lời:


128

Cách duy nhất có thể biên dịch điều này là nếu có một Num (Float,Float)thể hiện. Điều này không được cung cấp bởi thư viện tiêu chuẩn, mặc dù có thể một trong những thư viện bạn đang sử dụng đã thêm nó vì một số lý do điên rồ. Hãy thử tải dự án của bạn lên ghci và xem liệu có 10 :: (Float,Float)hoạt động hay không, sau đó thử :i Numtìm xem phiên bản đó đến từ đâu và sau đó la mắng bất cứ ai đã định nghĩa nó.

Phụ lục: Không có cách nào để tắt các phiên bản. Thậm chí không có cách nào để không xuất chúng từ một mô-đun. Nếu điều này có thể xảy ra, nó sẽ dẫn đến mã khó hiểu hơn . Giải pháp thực sự duy nhất ở đây là không xác định các trường hợp như vậy.


53
WOW. 10 :: (Float, Float)cho ra (10.0,10.0):i Numchứa dòng instance Num Point -- Defined in ‘Graphics.Gloss.Data.Point’( Pointlà bí danh Coord của Gloss). Nghiêm túc? Cảm ơn bạn. Điều đó đã cứu tôi khỏi một đêm mất ngủ.
Chất gây ung thư

6
@Carcigenicate Mặc dù có vẻ phù phiếm khi cho phép các trường hợp như vậy, nhưng lý do nó được phép là để các nhà phát triển có thể viết các trường hợp của riêng họ ở những Numnơi có ý nghĩa, chẳng hạn như Anglekiểu dữ liệu ràng buộc Doublegiữa -pipihoặc nếu ai đó muốn viết một kiểu dữ liệu đại diện cho quaternion hoặc một số kiểu số phức tạp hơn khác, tính năng này rất thuận tiện. Nó cũng tuân theo các quy tắc tương tự như String/ Text/ ByteString, cho phép các trường hợp này có ý nghĩa theo quan điểm dễ sử dụng, nhưng nó có thể bị sử dụng sai như trong trường hợp này.
bheklilr

4
@bheklilr Tôi hiểu sự cần thiết của việc cho phép các phiên bản của Num. "WOW" bắt nguồn từ một số điều. Tôi không biết bạn có thể tạo các phiên bản kiểu bí danh, việc tạo một phiên bản Num của Coord có vẻ hơi phản trực quan và tôi không nghĩ ra điều đó. Ồ, bài học kinh nghiệm.
Chất gây ung thư

3
Bạn có thể khắc phục sự cố của mình với phiên bản mồ côi từ thư viện của bạn bằng cách sử dụng newtypekhai báo Coordthay vì a type.
Benjamin Hodgson

3
@Carcigenicate Tôi tin rằng bạn cần -XTypeSynonymInstances để cho phép các trường hợp cho từ đồng nghĩa loại, nhưng điều đó không cần thiết để tạo ra trường hợp có vấn đề. Một ví dụ cho Num (Float, Float)hoặc thậm chí (Floating a) => Num (a,a)sẽ không yêu cầu phần mở rộng nhưng sẽ dẫn đến hành vi tương tự.
crockeea

64

Haskell's type checker là hợp lý. Vấn đề là các tác giả của một thư viện bạn đang sử dụng đã làm một điều gì đó ... kém hợp lý hơn.

Câu trả lời ngắn gọn là: Có, 10 :: (Float, Float)hoàn toàn hợp lệ nếu có một ví dụ Num (Float, Float). Không có gì "rất sai" về nó từ quan điểm của trình biên dịch hoặc ngôn ngữ. Nó không phù hợp với trực giác của chúng ta về những gì các chữ số làm. Vì bạn đã quen với kiểu hệ thống bắt loại lỗi mà bạn đã mắc phải, bạn hoàn toàn ngạc nhiên và thất vọng!

Numcác trường hợp và fromIntegervấn đề

Bạn ngạc nhiên rằng trình biên dịch chấp nhận 10 :: Coord, tức là 10 :: (Float, Float). Thật hợp lý khi giả định rằng các ký tự số như 10sẽ được suy ra là có kiểu "số". Ra khỏi hộp, literals số có thể được hiểu là Int, Integer, Float, hoặc Double. Một bộ số, không có ngữ cảnh khác, dường như không phải là một số theo cách mà bốn loại đó là số. Chúng tôi không nói về Complex.

Tuy nhiên, may mắn hay không may, Haskell là một ngôn ngữ rất linh hoạt. Tiêu chuẩn chỉ định rằng một số nguyên giống như chữ 10sẽ được hiểu là fromInteger 10, có kiểu Num a => a. Vì vậy, 10có thể được suy ra là bất kỳ kiểu nào có một Numphiên bản được viết cho nó. Tôi giải thích điều này chi tiết hơn một chút trong một câu trả lời khác .

Vì vậy, khi bạn đăng câu hỏi của mình, một Haskeller có kinh nghiệm ngay lập tức phát hiện ra rằng 10 :: (Float, Float)để được chấp nhận, phải có một ví dụ như Num a => Num (a, a)hoặc Num (Float, Float). Không có trường hợp như vậy trong Prelude, vì vậy nó phải được định nghĩa ở một nơi khác. Khi sử dụng :i Num, bạn nhanh chóng phát hiện ra nó đến từ đâu: glossgói.

Nhập từ đồng nghĩa và các trường hợp mồ côi

Nhưng hãy giữ trong một phút. Bạn không sử dụng bất kỳgloss loại trong ví dụ này; tại sao trường hợp trong glossảnh hưởng đến bạn? Câu trả lời có hai bước.

Đầu tiên, một từ đồng nghĩa loại được giới thiệu với từ khóa typekhông tạo ra một loại mới . Trong mô-đun của bạn, viết Coordđơn giản là viết tắt cho (Float, Float). Tương tự trong Graphics.Gloss.Data.Point, Pointcó nghĩa là (Float, Float). Nói cách khác, your Coordgloss's Pointthực sự tương đương.

Vì vậy, khi những người glossbảo trì chọn viết instance Num Point where ..., họ cũng biến Coordkiểu của bạn trở thành một ví dụ của Num. Điều đó tương đương với instance Num (Float, Float) where ...hoặc instance Num Coord where ....

(Theo mặc định, Haskell không cho phép các từ đồng nghĩa loại là phiên bản lớp. Các glosstác giả phải bật một cặp mở rộng ngôn ngữ,TypeSynonymInstancesFlexibleInstancesđể viết phiên bản.)

Thứ hai, điều này gây ngạc nhiên vì nó là một cá thể mồ côi , tức là một khai báo cá thể instance C Atrong đó cả hai CAđược định nghĩa trong các mô-đun khác. Ở đây nó là đặc biệt ngấm ngầm bởi vì mỗi phần có liên quan, ví dụ Num, (,)Float, xuất phát từ Preludevà có khả năng là trong phạm vi khắp mọi nơi.

Kỳ vọng của bạn là kỳ vọng được Numxác định trong Prelude, và bộ giá trị và Floatđược xác định trongPrelude , vì vậy mọi thứ về cách hoạt động của ba thứ đó đều được xác định trong Prelude. Tại sao việc nhập một mô-đun hoàn toàn khác sẽ thay đổi bất cứ điều gì? Lý tưởng nhất là không, nhưng các trường hợp mồ côi phá vỡ trực giác đó.

(Lưu ý rằng GHC cảnh báo về các trường hợp mồ côi — tác giả của gloss đè cảnh báo đó một cách cụ thể. Điều đó đáng lẽ phải giương cờ đỏ và nhắc ít nhất một cảnh báo trong tài liệu.)

Các phiên bản của lớp là toàn cầu và không thể ẩn

Hơn nữa, các cá thể lớp là toàn cục : bất kỳ cá thể nào được xác định trong bất kỳ mô-đun nào được nhập tạm thời từ mô-đun của bạn sẽ ở trong ngữ cảnh và có sẵn cho người đánh máy khi thực hiện phân giải cá thể. Điều này làm cho việc lập luận toàn cục trở nên thuận tiện, vì chúng ta có thể (thường) giả định rằng một hàm lớp như (+)sẽ luôn giống nhau đối với một kiểu nhất định. Tuy nhiên, nó cũng có nghĩa là các quyết định của địa phương có ảnh hưởng toàn cầu; xác định một cá thể lớp thay đổi không thể hủy ngang ngữ cảnh của mã hạ lưu, không có cách nào để che dấu hoặc che giấu nó đằng sau ranh giới mô-đun.

Bạn không thể sử dụng danh sách nhập để tránh nhập phiên bản . Tương tự, bạn không thể tránh xuất các phiên bản từ các mô-đun bạn xác định.

Đây là một vấn đề và được thảo luận nhiều trong lĩnh vực thiết kế ngôn ngữ Haskell. Có một cuộc thảo luận hấp dẫn về các vấn đề liên quan trong chuỗi reddit này . Ví dụ, hãy xem nhận xét của Edward Kmett về việc cho phép kiểm soát khả năng hiển thị đối với các trường hợp: "Về cơ bản, bạn đã loại bỏ tính đúng đắn của hầu hết các mã mà tôi đã viết."

(Nhân tiện, như câu trả lời này đã chứng minh , bạn có thể phá vỡ giả định về phiên bản toàn cục về một số mặt bằng cách sử dụng các cá thể mồ côi!)

Phải làm gì — cho những người triển khai thư viện

Suy nghĩ kỹ trước khi thực hiện Num. Bạn không thể làm việc xung quanh fromIntegervấn đề không, xác định fromInteger = error "not implemented"không không làm cho nó tốt hơn. Người dùng của bạn sẽ bối rối hoặc ngạc nhiên — hoặc tệ hơn, không bao giờ nhận thấy — nếu các ký tự số nguyên của họ vô tình được suy ra là có kiểu bạn đang tạo? Việc cung cấp (*)(+)điều đó có quan trọng không - đặc biệt nếu bạn phải hack nó?

Cân nhắc sử dụng các toán tử số học thay thế được xác định trong thư viện như của Conal Elliott vector-space(cho các loại *) hoặc Edward Kmett linear(cho các loại * -> *). Đây là những gì tôi có xu hướng tự làm.

Sử dụng -Wall. Không triển khai các phiên bản mồ côi và không tắt cảnh báo phiên bản mồ côi.

Ngoài ra, hãy làm theo hướng dẫn của linearvà nhiều thư viện hoạt động tốt khác và cung cấp các phiên bản mồ côi trong một mô-đun riêng biệt kết thúc bằng .OrphanInstanceshoặc .Instances. Và không nhập mô-đun đó từ bất kỳ mô-đun nào khác . Sau đó, người dùng có thể nhập các trẻ mồ côi một cách rõ ràng nếu họ muốn.

Nếu bạn thấy mình đang xác định trẻ mồ côi, hãy cân nhắc yêu cầu những người bảo trì thượng nguồn thực hiện chúng thay thế, nếu có thể và phù hợp. Tôi thường viết cá thể mồ côi Show a => Show (Identity a), cho đến khi họ thêm nó vào transformers. Tôi thậm chí có thể đã đưa ra một báo cáo lỗi về nó; Tôi không nhớ.

Phải làm gì — cho người tiêu dùng thư viện

Bạn không có nhiều lựa chọn. Tiếp cận — lịch sự và xây dựng! —Đến những người bảo trì thư viện. Chỉ cho họ câu hỏi này. Họ có thể có một số lý do đặc biệt để viết về đứa trẻ mồ côi có vấn đề, hoặc họ có thể không nhận ra.

Rộng hơn: Hãy nhận biết khả năng này. Đây là một trong số ít các lĩnh vực của Haskell nơi có các hiệu ứng toàn cầu thực sự; bạn phải kiểm tra xem mọi mô-đun bạn nhập và mọi mô-đun mà các mô-đun đó nhập, không triển khai các cá thể mồ côi. Các chú thích kiểu đôi khi có thể cảnh báo bạn về các vấn đề và tất nhiên bạn có thể sử dụng :itrong GHCi để kiểm tra.

Xác định chữ newtypes của riêng bạn thay vì typetừ đồng nghĩa nếu nó đủ quan trọng. Bạn có thể chắc chắn rằng không ai sẽ gây rối với họ.

Nếu bạn đang gặp sự cố thường xuyên xuất phát từ thư viện nguồn mở, tất nhiên bạn có thể tạo phiên bản thư viện của riêng mình, nhưng việc bảo trì có thể nhanh chóng trở thành vấn đề đau đầu.

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.