Mục đích của Rank2Types là gì?


110

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?


Về cơ bản, nó là một bản nâng cấp từ hệ thống loại HM lên giải tích lambda đa hình aka. λ2 / Hệ F. Hãy nhớ rằng kiểu suy luận không thể quyết định trong λ2.
Poscat

Câu trả lời:


116

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ụ mapsử dụng idas 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.


5
Để thêm một số từ kết nối câu trả lời của tôi với câu này: hãy xem xét hàm Haskell 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 anằ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 Intnhưng không String. Nếu chúng tôi bật, RankNTypeschú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ẽ glà gì?
Luis Casillas,

166

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 -> bdạ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 = Λtx: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 tvà 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 idcho 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ụ:

tx: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 ("→"). Rank2TypesRankNTypestắ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 rasau đó cung cấp đối số của loại kết quả. Vì vậy, bạn có thể chọn r = Inta = 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ờ ∀abê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' Intcho 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 Intvà 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 → Stringmột ẩn số a. Vâng, không biết đối với myObjectkhá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 Inthoặ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à ShowBoxgói cùng một giá trị của một số kiểu ẩn cùng với Showcá thể lớp của nó . Lưu ý rằng trong ví dụ ở dưới cùng, tôi tạo danh sách ShowBoxcó 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à ExistentialTypesGHC 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.


2
Cảm ơn vì một câu trả lời rất công phu! (trong đó, tình cờ, cũng cuối cùng thúc đẩy tôi để tìm hiểu lý thuyết loại thích hợp và hệ thống F.)
Aleksandar Dimitrov

5
Nếu bạn có existstừ 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 Anycũng có thể có một kiểu forall a. a -> Anyvà đó là nơi forallxuấ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).
Vitus

2
@Vitus: Sự tồn tại của GHC không bị ràng buộc với từ điển typeclass. Bạn có thể có 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 -> rx :: hcho 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.
Luis Casillas

Đó là một câu trả lời tuyệt vời mà tôi sẽ phải dành chút thời gian. Tôi nghĩ rằng tôi đã quá quen với những gì trừu tượng mà C # cung cấp, vì vậy tôi đã coi thường điều đó thay vì thực sự hiểu lý thuyết.
Andrey Shchekin

@sacundim: Chà, "tất cả các từ điển typeclass bắt buộc" cũng có thể có nghĩa là không có từ điển nào nếu bạn không cần. :) Quan điểm của tôi là GHC rất có thể không mã hóa các kiểu hiện sinh thông qua các kiểu được xếp hạng cao hơn (tức là phép biến đổi mà bạn đề xuất - ∃xP (x) ~ ∀r. (∀xP (x) → r) → r).
Vitus

47

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 IOhành động phóng tên lửa vào mục tiêu đó. Chúng tôi có thể đưa ra fmộ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 fmộ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 akhi chúng tôi gọi f, và chúng tôi chỉ chuyên biệt hóa nó Countryvà sử dụng \_ -> BestAllylạ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 \_ -> BestAllysẽ 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.


18

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 ở 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 IEmployeekhông được bày tỏ ý kiến ​​về việc Tphả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 Acceptphươ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.


1
Tôi rất muốn biết liệu có ai khác đã viết về sự hỗ trợ của C # / Java / Blub cho các loại cấp cao hơn hay không. Nếu bạn, độc giả thân yêu, biết bất kỳ tài nguyên nào như vậy, vui lòng gửi chúng theo cách của tôi!
Benjamin Hodgson


-2

Đố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 Identifyyêu cầu một hàm chung của kiểu Identifiernhư thế nào? Điều này làm cho Identifymột chức năng cấp cao hơn.


Điều này bổ sung gì cho câu trả lời của sepp2k?
dfeuer 27/07/17

Hay của Benjamin Hodgson, vì vấn đề đó?
dfeuer 27/07/17

1
Tôi nghĩ bạn đã bỏ qua quan điểm của Hodgson. Acceptcó 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 Acceptbất kỳ phương pháp nào.
dfeuer

1
Ví dụ của bạn cũng là hạng 2, theo Visiteelớp bạn giới thiệu. Về cơ bản, một hàm f :: Visitee e => T elà (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.
dfeuer

1
Bạn không thể làm nổi foralltrong 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.
dfeuer
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.