Ở đây, "giá trị", "loại" và "loại" có ý nghĩa chính thức, vì vậy việc xem xét cách sử dụng tiếng Anh phổ biến hoặc tương tự để phân loại ô tô sẽ chỉ đưa bạn đến nay.
Câu trả lời của tôi liên quan đến ý nghĩa chính thức của các thuật ngữ này trong bối cảnh của Haskell cụ thể; những ý nghĩa này dựa trên (mặc dù không thực sự giống với) những ý nghĩa được sử dụng trong "lý thuyết loại" toán học / CS. Vì vậy, đây sẽ không phải là một câu trả lời "khoa học máy tính" rất tốt, nhưng nó sẽ đóng vai trò là một câu trả lời khá hay của Haskell.
Trong Haskell (và các ngôn ngữ khác), việc gán một loại cho biểu thức chương trình mô tả lớp giá trị mà biểu thức được phép có là điều hữu ích . Tôi giả sử ở đây rằng bạn đã thấy đủ các ví dụ để hiểu lý do tại sao sẽ hữu ích khi biết rằng trong biểu thức sqrt (a**2 + b**2)
, các biến a
và b
sẽ luôn là các giá trị của kiểu Double
và không, nói, String
và Bool
tương ứng. Về cơ bản, có các loại hỗ trợ chúng ta viết các biểu thức / chương trình sẽ hoạt động chính xác trên một loạt các giá trị .
Bây giờ, một điều bạn có thể không nhận ra là các loại Haskell, giống như các loại xuất hiện trong chữ ký loại:
fmap :: Functor f => (a -> b) -> f a -> f b
thực ra chính chúng được viết bằng một ngôn ngữ con Haskell cấp độ loại. Văn bản chương trình Functor f => (a -> b) -> f a -> f b
là - hoàn toàn theo nghĩa đen - một biểu thức kiểu được viết bằng ngôn ngữ con này. Các sublanguage bao gồm các nhà khai thác (ví dụ, ->
là một nhà điều hành ghi kết hợp ngay trong ngôn ngữ này), các biến (ví dụ f
, a
và b
), và "ứng dụng" của một biểu thức kiểu khác (ví dụ, f a
được f
áp dụng cho a
).
Tôi đã đề cập đến việc làm thế nào hữu ích trong nhiều ngôn ngữ để gán các loại cho các biểu thức chương trình để mô tả các lớp của các giá trị biểu thức? Vâng, trong ngôn ngữ con cấp độ loại này, các biểu thức đánh giá các loại (chứ không phải giá trị ) và cuối cùng sẽ hữu ích khi gán các loại cho biểu thức loại để mô tả các loại loại mà chúng được phép đại diện. Về cơ bản, có các loại hỗ trợ chúng ta bằng cách viết các biểu thức loại sẽ hoạt động chính xác trên một loạt các loại .
Vì vậy, các giá trị là loại như loại là loại và loại giúp chúng ta viết chương trình giá trị -level trong khi loại giúp chúng ta viết chương trình loại -level.
Những loại này trông như thế nào? Vâng, hãy xem xét chữ ký loại:
id :: a -> a
Nếu biểu thức loại a -> a
là có giá trị, những gì loại của các loại chúng ta nên cho phép biến a
được? Vâng, các biểu thức loại:
Int -> Int
Bool -> Bool
Nhìn hợp lệ, vì vậy các loại Int
và Bool
rõ ràng là đúng loại . Nhưng thậm chí các loại phức tạp hơn như:
[Double] -> [Double]
Maybe [(Double,Int)] -> Maybe [(Double,Int)]
nhìn hợp lệ Trong thực tế, vì chúng ta phải có khả năng gọi các id
chức năng, thậm chí:
(a -> a) -> (a -> a)
nhìn ổn. Vì vậy, Int
, Bool
, [Double]
, Maybe [(Double,Int)]
, và a -> a
tất cả nhìn như loại quyền loại .
Nói cách khác, có vẻ như chỉ có một loại , hãy gọi nó *
giống như một ký tự đại diện Unix và mọi loại đều có cùng một loại *
, kết thúc câu chuyện.
Đúng?
Vâng, không hoàn toàn. Nó chỉ ra rằng Maybe
, tất cả chính nó, chỉ là một biểu thức kiểu hợp lệ Maybe Int
(theo nhiều cách giống nhau sqrt
, tất cả đều giống như một biểu thức giá trị như sqrt 25
). Tuy nhiên , biểu thức loại sau không hợp lệ:
Maybe -> Maybe
Bởi vì, trong khi Maybe
là một biểu thức kiểu, nó không đại diện cho loại của loại đó có thể có giá trị. Vì vậy, đó là cách chúng ta nên xác định *
- đó là loại của các loại có giá trị; nó bao gồm các loại "hoàn thành" như Double
hoặc Maybe [(Double,Int)]
nhưng loại trừ các loại không hoàn chỉnh, không có giá trị như Either String
. Để đơn giản, tôi sẽ gọi những loại hoàn chỉnh này là *
" loại cụ thể", mặc dù thuật ngữ này không phổ biến và "loại cụ thể" có thể có nghĩa gì đó rất khác với một lập trình viên C ++.
Bây giờ, trong biểu thức kiểu a -> a
, miễn là loại a
có loại *
(loại bê tông), kết quả của biểu thức loại cũnga -> a
sẽ có loại (nghĩa là loại bê tông). *
Vì vậy, những gì loại của loại là Maybe
? Vâng, Maybe
có thể được áp dụng cho một loại bê tông để mang lại một loại bê tông khác. Vì vậy, Maybe
trông giống như một chút giống như một loại chức năng cấp mà phải mất một kiểu của loại *
và trả về một kiểu của loại *
. Nếu chúng ta có hàm mức giá trị lấy giá trị của loại Int
và trả về giá trị của loại Int
, chúng ta sẽ cung cấp cho nó một chữ ký loạiInt -> Int
, do đó, bằng cách tương tự, chúng ta nên đưa ra Maybe
một chữ ký loại* -> *
. GHCi đồng ý:
> :kind Maybe
Maybe :: * -> *
Sẽ trở lại:
fmap :: Functor f => (a -> b) -> f a -> f b
Trong loại chữ ký này, biến f
có loại * -> *
và biến a
và b
có loại *
; toán tử tích hợp ->
có loại * -> * -> *
(nó lấy một loại *
ở bên trái và một ở bên phải và trả về một loại cũng có loại *
). Từ điều này và các quy tắc suy luận loại, bạn có thể suy ra đó a -> b
là một loại hợp lệ với loại *
, f a
và f b
cũng là loại hợp lệ với loại *
, và (a -> b) -> f a -> f b
là loại hợp lệ *
.
Nói cách khác, trình biên dịch có thể "kiểm tra loại" biểu thức loại (a -> b) -> f a -> f b
để xác minh nó hợp lệ cho các biến loại đúng loại giống như cách "kiểm tra kiểu" sqrt (a**2 + b**2)
để xác minh nó hợp lệ cho các biến đúng loại.
Lý do sử dụng các thuật ngữ riêng cho "loại" so với "loại" (nghĩa là không nói về "loại loại") chủ yếu chỉ để tránh nhầm lẫn. Các loại ở trên trông rất khác với các loại và, ít nhất là lúc đầu, dường như cư xử khá khác nhau. (Ví dụ, phải mất một thời gian để quấn quanh đầu của bạn xung quanh ý tưởng rằng mỗi "bình thường" loại có cùng loại *
và các loại a -> b
là *
không * -> *
.)
Một số điều này cũng là lịch sử. Khi GHC Haskell đã phát triển, sự khác biệt giữa các giá trị, loại và loại đã bắt đầu mờ đi. Ngày nay, các giá trị có thể được "quảng bá" thành các loại, và các loại và các loại thực sự giống nhau. Vì vậy, trong Haskell hiện đại, giá trị cả hai đều có chủng loại và LÀ loại (hầu như), và các loại định dạng nhiều loại chỉ hơn.
@ user21820 yêu cầu một số giải thích bổ sung về "các loại và loại thực sự giống nhau". Nói rõ hơn một chút, trong GHC Haskell hiện đại (kể từ phiên bản 8.0.1, tôi nghĩ), các loại và loại được xử lý thống nhất trong hầu hết các mã trình biên dịch. Trình biên dịch thực hiện một số nỗ lực trong các thông báo lỗi để phân biệt giữa "loại" và "loại", tùy thuộc vào việc nó phàn nàn về loại giá trị hoặc loại tương ứng.
Ngoài ra, nếu không có tiện ích mở rộng nào được bật, chúng có thể dễ dàng phân biệt bằng ngôn ngữ bề mặt. Ví dụ: các loại (của các giá trị) có một biểu diễn trong cú pháp (ví dụ: trong các chữ ký loại), nhưng các loại (các loại) là - tôi nghĩ - hoàn toàn ẩn và không có cú pháp rõ ràng nơi chúng xuất hiện.
Nhưng, nếu bạn bật các tiện ích mở rộng phù hợp, sự khác biệt giữa các loại và phần lớn sẽ biến mất. Ví dụ:
{-# LANGUAGE GADTs, TypeInType #-}
data Foo where
Bar :: Bool -> * -> Foo
Ở đây, Bar
là (cả một giá trị và) một loại. Là một loại, loại của nó Bool -> * -> Foo
, là một hàm cấp loại có một loại Bool
(là một loại, nhưng cũng là một loại) và một loại *
và tạo ra một loại Foo
. Vì thế:
type MyBar = Bar True Int
kiểm tra loại chính xác.
Như @AndrejBauer giải thích trong câu trả lời của mình, việc không phân biệt được loại và loại này là không an toàn - có một loại / loại *
có loại / loại chính nó (đó là trường hợp của Haskell hiện đại) dẫn đến nghịch lý. Tuy nhiên, hệ thống loại của Haskell đã đầy những nghịch lý vì không chấm dứt, vì vậy nó không được coi là một vấn đề lớn.