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!
Num
các trường hợp và fromInteger
vấ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ư 10
sẽ đượ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ữ 10
sẽ được hiểu là fromInteger 10
, có kiểu Num a => a
. Vì vậy, 10
có thể được suy ra là bất kỳ kiểu nào có một Num
phiê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: gloss
gó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 type
khô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
, Point
có nghĩa là (Float, Float)
. Nói cách khác, your Coord
và gloss
's Point
thực sự tương đương.
Vì vậy, khi những người gloss
bảo trì chọn viết instance Num Point where ...
, họ cũng biến Coord
kiể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 gloss
tác giả phải bật một cặp mở rộng ngôn ngữ,TypeSynonymInstances
và FlexibleInstances
để 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 A
trong đó cả hai C
và A
đượ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
, (,)
và Float
, xuất phát từ Prelude
và 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 Num
xá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 fromInteger
vấ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 (*)
và (+)
đ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 linear
và 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 .OrphanInstances
hoặ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 :i
trong GHCi để kiểm tra.
Xác định chữ newtype
s của riêng bạn thay vì type
từ đồ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.