Tôi không thực sự thành thạo Haskell, vì vậy đây có thể là một câu hỏi rất dễ.
Rank2Types giải quyết giới hạn ngôn ngữ nào ? Các hàm trong Haskell không hỗ trợ các đối số đa hình sao?
Tôi không thực sự thành thạo Haskell, vì vậy đây có thể là một câu hỏi rất dễ.
Rank2Types giải quyết giới hạn ngôn ngữ nào ? Các hàm trong Haskell không hỗ trợ các đối số đa hình sao?
Câu trả lời:
Các hàm trong Haskell không hỗ trợ các đối số đa hình?
Chúng có, nhưng chỉ ở hạng 1. Điều này có nghĩa là trong khi bạn có thể viết một hàm nhận các loại đối số khác nhau mà không có phần mở rộng này, bạn không thể viết một hàm sử dụng đối số của nó như các loại khác nhau trong cùng một lệnh gọi.
Ví dụ: không thể nhập hàm sau mà không có phần mở rộng này vì g
được sử dụng với các loại đối số khác nhau trong định nghĩa của f
:
f g = g 1 + g "lala"
Lưu ý rằng hoàn toàn có thể truyền một hàm đa hình làm đối số cho một hàm khác. Vì vậy, một cái gì đó như map id ["a","b","c"]
là hoàn toàn hợp pháp. Nhưng hàm chỉ có thể sử dụng nó dưới dạng đơn hình. Trong ví dụ map
sử dụng id
as if it has type String -> String
. Và tất nhiên bạn cũng có thể chuyển một hàm đơn hình đơn giản của kiểu đã cho thay vì id
. Nếu không có kiểu rank2 thì không có cách nào để một hàm yêu cầu đối số của nó phải là một hàm đa hình và do đó cũng không có cách nào để sử dụng nó như một hàm đa hình.
f' g x y = g x + g y
. Loại xếp hạng 1 được suy ra của nó là forall a r. Num r => (a -> r) -> a -> a -> r
. Vì forall a
nằm ngoài các mũi tên chức năng, người gọi trước tiên phải chọn một loại cho a
; nếu họ chọn Int
, chúng tôi nhận được f' :: forall r. Num r => (Int -> r) -> Int -> Int -> r
, và bây giờ chúng tôi đã sửa g
đối số để nó có thể mất Int
nhưng không String
. Nếu chúng tôi bật, RankNTypes
chúng tôi có thể chú thích f'
bằng loại forall b c r. Num r => (forall a. a -> r) -> b -> c -> r
. Tuy nhiên, không thể sử dụng nó — sẽ g
là gì?
Thật khó để hiểu tính đa hình cấp cao hơn trừ khi bạn nghiên cứu trực tiếp Hệ thống F , bởi vì Haskell được thiết kế để giấu các chi tiết của điều đó với bạn vì sự đơn giản.
Nhưng về cơ bản, ý tưởng sơ bộ là các kiểu đa hình không thực sự có a -> b
dạng như trong Haskell; trong thực tế, chúng trông như thế này, luôn luôn có các bộ định lượng rõ ràng:
id :: ∀a.a → a
id = Λt.λx:t.x
Nếu bạn không biết ký hiệu "∀", nó được đọc là "cho tất cả"; ∀x.dog(x)
có nghĩa là "với mọi x, x là một con chó." "Λ" là lambda viết hoa, được sử dụng để trừu tượng hóa các tham số kiểu; dòng thứ hai cho biết id là một hàm nhận một kiểu t
và sau đó trả về một hàm được tham số hóa bởi kiểu đó.
Bạn thấy đấy, trong Hệ thống F, bạn không thể chỉ áp dụng một hàm như vậy id
cho một giá trị ngay lập tức; trước tiên, bạn cần áp dụng hàm to cho một kiểu để có được hàm λ mà bạn áp dụng cho một giá trị. Ví dụ:
(Λt.λx:t.x) Int 5 = (λx:Int.x) 5
= 5
Tiêu chuẩn Haskell (tức là Haskell 98 và 2010) đơn giản hóa điều này cho bạn bằng cách không có bất kỳ bộ định lượng kiểu, lambdas viết hoa và ứng dụng kiểu nào, nhưng đằng sau GHC đưa chúng vào khi phân tích chương trình để biên dịch. (Tôi tin rằng đây là tất cả nội dung thời gian biên dịch, không có chi phí thời gian chạy.)
Nhưng việc Haskell tự động xử lý điều này có nghĩa là nó giả định rằng "∀" không bao giờ xuất hiện ở nhánh bên trái của kiểu hàm ("→"). Rank2Types
và RankNTypes
tắt các hạn chế đó và cho phép bạn ghi đè các quy tắc mặc định của Haskell về vị trí cần chèn forall
.
Tại sao bạn muốn làm điều này? Bởi vì Hệ thống F đầy đủ, không bị giới hạn rất mạnh mẽ và nó có thể làm được rất nhiều điều thú vị. Ví dụ: ẩn kiểu và mô-đun có thể được thực hiện bằng cách sử dụng các kiểu cấp cao hơn. Lấy ví dụ một hàm cũ đơn giản thuộc loại xếp hạng 1 sau (để thiết lập cảnh):
f :: ∀r.∀a.((a → r) → a → r) → r
Để sử dụng f
, trước tiên người gọi phải chọn loại nào để sử dụng r
và a
sau đó cung cấp đối số của loại kết quả. Vì vậy, bạn có thể chọn r = Int
và a = String
:
f Int String :: ((String → Int) → String → Int) → Int
Nhưng bây giờ hãy so sánh nó với loại cấp cao hơn sau:
f' :: ∀r.(∀a.(a → r) → a → r) → r
Làm thế nào để một chức năng của loại này hoạt động? Để sử dụng nó, trước tiên bạn chỉ định loại để sử dụng r
. Giả sử chúng tôi chọn Int
:
f' Int :: (∀a.(a → Int) → a → Int) → Int
Nhưng bây giờ ∀a
là bên trong mũi tên chức năng, vì vậy bạn không thể chọn loại để sử dụng a
; bạn phải áp dụng f' Int
cho một hàm Λ thuộc loại thích hợp. Điều này có nghĩa là việc triển khai f'
sẽ chọn loại để sử dụng a
, không phải người gọif'
. Không có các loại cấp cao hơn, ngược lại, người gọi luôn chọn các loại.
Điều này có ích gì? Thực ra, đối với nhiều thứ, nhưng một ý tưởng là bạn có thể sử dụng điều này để mô hình hóa những thứ như lập trình hướng đối tượng, trong đó "đối tượng" gói một số dữ liệu ẩn cùng với một số phương thức hoạt động trên dữ liệu ẩn. Vì vậy, ví dụ, một đối tượng có hai phương thức — một phương thức trả về một Int
và một phương thức khác trả về a String
, có thể được triển khai với kiểu này:
myObject :: ∀r.(∀a.(a → Int, a -> String) → a → r) → r
Cái này hoạt động ra sao? Đối tượng được thực hiện dưới dạng một hàm có một số dữ liệu bên trong thuộc kiểu ẩn a
. Để thực sự sử dụng đối tượng, các máy khách của nó truyền vào một hàm "gọi lại" mà đối tượng sẽ gọi bằng hai phương thức. Ví dụ:
myObject String (Λa. λ(length, name):(a → Int, a → String). λobjData:a. name objData)
Ở đây, về cơ bản, chúng ta đang gọi phương thức thứ hai của đối tượng, phương thức có kiểu là a → String
một ẩn số a
. Vâng, không biết đối với myObject
khách hàng của; nhưng những khách hàng này biết, từ chữ ký, rằng họ sẽ có thể áp dụng một trong hai chức năng cho nó và nhận được một Int
hoặc một String
.
Đối với một ví dụ Haskell thực tế, dưới đây là đoạn mã mà tôi đã viết khi tôi tự học RankNTypes
. Điều này thực hiện một kiểu được gọi là ShowBox
gói cùng một giá trị của một số kiểu ẩn cùng với Show
cá thể lớp của nó . Lưu ý rằng trong ví dụ ở dưới cùng, tôi tạo danh sách ShowBox
có phần tử đầu tiên được tạo từ một số và phần tử thứ hai từ một chuỗi. Vì các loại được ẩn bằng cách sử dụng các loại cấp cao hơn, điều này không vi phạm việc kiểm tra loại.
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ImpredicativeTypes #-}
type ShowBox = forall b. (forall a. Show a => a -> b) -> b
mkShowBox :: Show a => a -> ShowBox
mkShowBox x = \k -> k x
-- | This is the key function for using a 'ShowBox'. You pass in
-- a function @k@ that will be applied to the contents of the
-- ShowBox. But you don't pick the type of @k@'s argument--the
-- ShowBox does. However, it's restricted to picking a type that
-- implements @Show@, so you know that whatever type it picks, you
-- can use the 'show' function.
runShowBox :: forall b. (forall a. Show a => a -> b) -> ShowBox -> b
-- Expanded type:
--
-- runShowBox
-- :: forall b. (forall a. Show a => a -> b)
-- -> (forall b. (forall a. Show a => a -> b) -> b)
-- -> b
--
runShowBox k box = box k
example :: [ShowBox]
-- example :: [ShowBox] expands to this:
--
-- example :: [forall b. (forall a. Show a => a -> b) -> b]
--
-- Without the annotation the compiler infers the following, which
-- breaks in the definition of 'result' below:
--
-- example :: forall b. [(forall a. Show a => a -> b) -> b]
--
example = [mkShowBox 5, mkShowBox "foo"]
result :: [String]
result = map (runShowBox show) example
Tái bút: đối với bất kỳ ai đang đọc điều này, những người tự hỏi làm thế nào mà ExistentialTypes
GHC sử dụng forall
, tôi tin rằng lý do là vì nó sử dụng loại kỹ thuật này đằng sau hậu trường.
exists
từ khóa, bạn có thể xác định một kiểu tồn tại là (ví dụ) data Any = Any (exists a. a)
, ở đâu Any :: (exists a. a) -> Any
. Sử dụng ∀xP (x) → Q ≡ (∃xP (x)) → Q, chúng ta có thể kết luận rằng Any
cũng có thể có một kiểu forall a. a -> Any
và đó là nơi forall
xuất phát từ khóa. Tôi tin rằng các kiểu hiện sinh do GHC triển khai chỉ là kiểu dữ liệu thông thường cũng mang tất cả các từ điển typeclass bắt buộc (tôi không thể tìm thấy tài liệu tham khảo để sao lưu điều này, xin lỗi).
data ApplyBox r = forall a. ApplyBox (a -> r) a
; khi bạn khớp với mẫu ApplyBox f x
, bạn nhận được f :: h -> r
và x :: h
cho một loại bị hạn chế "ẩn" h
. Nếu tôi hiểu đúng, trường hợp từ điển typeclass được dịch thành một cái gì đó như thế này: data ShowBox = forall a. Show a => ShowBox a
được dịch sang một cái gì đó như data ShowBox' = forall a. ShowBox' (ShowDict' a) a
; instance Show ShowBox' where show (ShowBox' dict val) = show' dict val
; show' :: ShowDict a -> a -> String
.
Câu trả lời của Luis Casillas cung cấp rất nhiều thông tin tuyệt vời về ý nghĩa của loại hạng 2, nhưng tôi sẽ chỉ mở rộng một điểm mà anh ấy chưa đề cập đến. Việc yêu cầu một đối số là đa hình không chỉ cho phép nó được sử dụng với nhiều kiểu; nó cũng hạn chế những gì hàm đó có thể làm với (các) đối số của nó và cách nó có thể tạo ra kết quả. Đó là, nó mang lại cho người gọi ít hơn linh hoạt . Tại sao bạn muốn làm điều đó? Tôi sẽ bắt đầu với một ví dụ đơn giản:
Giả sử chúng ta có một kiểu dữ liệu
data Country = BigEnemy | MediumEnemy | PunyEnemy | TradePartner | Ally | BestAlly
và chúng tôi muốn viết một hàm
f g = launchMissilesAt $ g [BigEnemy, MediumEnemy, PunyEnemy]
có một chức năng được cho là chọn một trong các phần tử của danh sách mà nó đưa ra và trả về một IO
hành động phóng tên lửa vào mục tiêu đó. Chúng tôi có thể đưa ra f
một loại đơn giản:
f :: ([Country] -> Country) -> IO ()
Vấn đề là chúng tôi có thể vô tình chạy
f (\_ -> BestAlly)
và sau đó chúng tôi sẽ gặp rắc rối lớn! Cho f
một loại đa hình bậc 1
f :: ([a] -> a) -> IO ()
hoàn toàn không giúp ích gì, bởi vì chúng tôi chọn loại a
khi chúng tôi gọi f
, và chúng tôi chỉ chuyên biệt hóa nó Country
và sử dụng \_ -> BestAlly
lại mã độc hại của chúng tôi . Giải pháp là sử dụng loại xếp hạng 2:
f :: (forall a . [a] -> a) -> IO ()
Bây giờ hàm chúng ta truyền vào bắt buộc phải là đa hình, vì vậy \_ -> BestAlly
sẽ không nhập kiểm tra! Trên thực tế, không có hàm nào trả về một phần tử không có trong danh sách mà nó được đưa ra sẽ đánh máy (mặc dù một số hàm đi vào vòng lặp vô hạn hoặc tạo ra lỗi và do đó không bao giờ trả về sẽ làm như vậy).
Tất nhiên, ở trên là giả thuyết, nhưng một biến thể của kỹ thuật này là chìa khóa để làm cho ST
đơn nguyên an toàn.
Các loại hạng cao hơn không kỳ lạ như các câu trả lời khác đã đưa ra. Tin hay không thì tùy, nhiều ngôn ngữ hướng đối tượng (bao gồm cả Java và C #!) Có tính năng của chúng. (Tất nhiên, không ai trong những cộng đồng đó biết đến họ bằng cái tên nghe có vẻ đáng sợ "các loại cấp cao hơn".)
Ví dụ tôi sẽ đưa ra là một sách giáo khoa triển khai mẫu Khách truy cập, mà tôi sử dụng mọi lúc trong công việc hàng ngày của mình. Câu trả lời này không nhằm mục đích giới thiệu mẫu khách truy cập; kiến thức đó sẵn có ở những nơi khác .
Trong ứng dụng nhân sự tưởng tượng quan trọng này, chúng tôi muốn vận hành trên những nhân viên có thể là nhân viên chính thức toàn thời gian hoặc nhà thầu tạm thời. Biến thể ưa thích của tôi của mẫu Khách truy cập (và thực sự là biến thể có liên quan đến RankNTypes
) tham số kiểu quay lại của khách truy cập.
interface IEmployeeVisitor<T>
{
T Visit(PermanentEmployee e);
T Visit(Contractor c);
}
class XmlVisitor : IEmployeeVisitor<string> { /* ... */ }
class PaymentCalculator : IEmployeeVisitor<int> { /* ... */ }
Vấn đề là một số khách truy cập với các kiểu trả lại khác nhau đều có thể hoạt động trên cùng một dữ liệu. Phương tiện này IEmployee
không được bày tỏ ý kiến về việc T
phải như thế nào.
interface IEmployee
{
T Accept<T>(IEmployeeVisitor<T> v);
}
class PermanentEmployee : IEmployee
{
// ...
public T Accept<T>(IEmployeeVisitor<T> v)
{
return v.Visit(this);
}
}
class Contractor : IEmployee
{
// ...
public T Accept<T>(IEmployeeVisitor<T> v)
{
return v.Visit(this);
}
}
Tôi muốn thu hút sự chú ý của bạn đến các loại. Quan sát rằng IEmployeeVisitor
định lượng phổ biến kiểu trả về của nó, trong khi IEmployee
định lượng nó bên trong Accept
phương pháp của nó - có nghĩa là, ở một thứ hạng cao hơn. Dịch lộn xộn từ C # sang Haskell:
data IEmployeeVisitor r = IEmployeeVisitor {
visitPermanent :: PermanentEmployee -> r,
visitContractor :: Contractor -> r
}
newtype IEmployee = IEmployee {
accept :: forall r. IEmployeeVisitor r -> r
}
Vì vậy, bạn có nó. Các kiểu cấp cao hơn hiển thị trong C # khi bạn viết các kiểu có chứa các phương thức chung.
Các slide từ khóa học Haskell của Bryan O'Sullivan tại Stanford đã giúp tôi hiểu Rank2Types
.
Đối với những người quen thuộc với ngôn ngữ hướng đối tượng, một hàm cấp cao hơn chỉ đơn giản là một hàm chung mong đợi như đối số của nó là một hàm chung khác.
Ví dụ: trong TypeScript, bạn có thể viết:
type WithId<T> = T & { id: number }
type Identifier = <T>(obj: T) => WithId<T>
type Identify = <TObj>(obj: TObj, f: Identifier) => WithId<TObj>
Hãy xem kiểu hàm chung Identify
yêu cầu một hàm chung của kiểu Identifier
như thế nào? Điều này làm cho Identify
một chức năng cấp cao hơn.
Accept
có kiểu đa hình bậc 1, nhưng đó là một phương thức của IEmployee
, bản thân nó là bậc 2. Nếu ai đó cho tôi một IEmployee
, tôi có thể mở nó ra và sử dụng Accept
bất kỳ phương pháp nào.
Visitee
lớp bạn giới thiệu. Về cơ bản, một hàm f :: Visitee e => T e
là (sau khi nội dung lớp được gỡ bỏ) f :: (forall r. e -> Visitor e r -> r) -> T e
. Haskell 2010 cho phép bạn thoát khỏi tính đa hình bậc 2 hạn chế bằng cách sử dụng các lớp như vậy.
forall
trong ví dụ của tôi. Tôi không có tài liệu tham khảo, nhưng bạn cũng có thể tìm thấy thứ gì đó trong "Scrap Your Type Classes" . Tính đa hình cấp cao hơn thực sự có thể đưa ra các vấn đề kiểm tra kiểu, nhưng sắp xếp giới hạn ngầm hiểu trong hệ thống lớp là tốt.