จุดประสงค์ของ Rank2Types คืออะไร?


110

ฉันไม่ค่อยเชี่ยวชาญเรื่อง Haskell ดังนั้นนี่อาจเป็นคำถามที่ง่ายมาก

Rank2Typesแก้ข้อ จำกัด ด้านภาษาอะไรบ้าง? ฟังก์ชั่นใน Haskell ไม่รองรับอาร์กิวเมนต์ polymorphic แล้วหรือ?


โดยพื้นฐานแล้วเป็นการอัพเกรดจากระบบประเภท HM เป็นแคลคูลัส polymorphic lambda aka λ2 / ระบบ F. โปรดทราบว่าการอนุมานประเภทไม่สามารถตัดสินใจได้ในλ2
Poscat

คำตอบ:


117

ฟังก์ชั่นใน Haskell ไม่รองรับอาร์กิวเมนต์แบบหลายรูปแบบหรือไม่?

พวกเขาทำ แต่อันดับ 1 เท่านั้นซึ่งหมายความว่าในขณะที่คุณสามารถเขียนฟังก์ชันที่รับอาร์กิวเมนต์ประเภทต่างๆโดยไม่มีส่วนขยายนี้คุณจะไม่สามารถเขียนฟังก์ชันที่ใช้อาร์กิวเมนต์เป็นประเภทต่างๆในการเรียกใช้เดียวกันได้

ตัวอย่างเช่นไม่สามารถพิมพ์ฟังก์ชันต่อไปนี้หากไม่มีส่วนขยายนี้เนื่องจากgใช้กับอาร์กิวเมนต์ประเภทต่างๆในนิยามของf:

f g = g 1 + g "lala"

โปรดทราบว่าเป็นไปได้อย่างสมบูรณ์ที่จะส่งผ่านฟังก์ชัน polymorphic เป็นอาร์กิวเมนต์ไปยังฟังก์ชันอื่น ดังนั้นสิ่งที่ชอบmap id ["a","b","c"]คือกฎหมายอย่างสมบูรณ์ แต่ฟังก์ชั่นนี้สามารถใช้เป็น monomorphic เท่านั้น ในตัวอย่างmapใช้ราวกับว่ามันมีประเภทid String -> Stringและแน่นอนคุณยังสามารถส่งผ่านฟังก์ชั่นที่เรียบง่ายของ monomorphic idประเภทรับแทน หากไม่มี rank2types ไม่มีทางที่ฟังก์ชันจะกำหนดให้อาร์กิวเมนต์ของมันต้องเป็นฟังก์ชัน polymorphic ดังนั้นจึงไม่มีวิธีใดที่จะใช้เป็นฟังก์ชัน polymorphic ได้


5
หากต้องการเพิ่มคำบางคำเชื่อมต่อคำตอบของฉันคนนี้: f' g x y = g x + g yพิจารณาฟังก์ชัน ที่สรุปอันดับที่ 1 forall a r. Num r => (a -> r) -> a -> a -> rประเภทของมันคือ เนื่องจากforall aอยู่นอกลูกศรของฟังก์ชันก่อนอื่นผู้โทรจะต้องเลือกประเภทสำหรับa; ถ้าพวกเขาเลือกIntที่เราได้รับf' :: forall r. Num r => (Int -> r) -> Int -> Int -> rและตอนนี้เรามีการแก้ไขgข้อโต้แย้งเพื่อที่จะสามารถใช้เวลาแต่ไม่Int Stringถ้าเราช่วยให้RankNTypesเราสามารถใส่คำอธิบายประกอบกับชนิดf' forall b c r. Num r => (forall a. a -> r) -> b -> c -> rใช้ไม่ได้ - จะgเป็นอย่างไร
Luis Casillas

166

เป็นเรื่องยากที่จะเข้าใจความแตกต่างที่มีอันดับสูงกว่าเว้นแต่คุณจะศึกษาSystem Fโดยตรงเนื่องจาก Haskell ได้รับการออกแบบมาเพื่อซ่อนรายละเอียดของสิ่งนั้นจากคุณเพื่อความเรียบง่าย

แต่โดยพื้นฐานแล้วแนวคิดคร่าวๆก็คือประเภทของโพลีมอร์ฟิกไม่ได้มีa -> bรูปแบบที่พวกเขาทำในฮัสเคลล์ ในความเป็นจริงพวกมันมีลักษณะเช่นนี้เสมอโดยมีตัวระบุปริมาณที่ชัดเจน:

id :: a.a  a
id = Λtx:t.x

หากคุณไม่ทราบสัญลักษณ์ "∀" แสดงว่า "สำหรับทั้งหมด" ∀x.dog(x)หมายความว่า "สำหรับ x ทั้งหมด x คือสุนัข" "Λ" คือตัวพิมพ์ใหญ่แลมบ์ดาใช้สำหรับการแยกพารามิเตอร์ประเภท สิ่งที่บรรทัดที่สองกล่าวคือ id คือฟังก์ชันที่รับประเภทtแล้วส่งกลับฟังก์ชันที่พาราเมตไตรด์ตามประเภทนั้น

คุณจะเห็นว่าในระบบ F คุณไม่สามารถใช้ฟังก์ชันแบบนั้นidกับค่าได้ทันที ก่อนอื่นคุณต้องใช้Λ-function กับชนิดเพื่อให้ได้λ-function ที่คุณใช้กับค่า ตัวอย่างเช่น:

tx:t.x) Int 5 = x:Int.x) 5
                  = 5

Standard Haskell (เช่น Haskell 98 และ 2010) ทำให้สิ่งนี้ง่ายขึ้นสำหรับคุณโดยไม่มีตัวระบุประเภทใด ๆ เหล่านี้ lambdas ตัวพิมพ์ใหญ่และแอปพลิเคชั่นประเภท แต่เบื้องหลัง GHC จะนำมาใช้เมื่อวิเคราะห์โปรแกรมเพื่อรวบรวม (ฉันเชื่อว่านี่คือสิ่งที่รวบรวมเวลาทั้งหมดโดยไม่มีค่าใช้จ่ายรันไทม์)

แต่การจัดการโดยอัตโนมัติของ Haskell หมายความว่าสมมติว่า "∀" ไม่เคยปรากฏที่สาขาด้านซ้ายมือของประเภทฟังก์ชัน ("→") Rank2TypesและRankNTypesปิดข้อ จำกัด เหล่านั้นและช่วยให้คุณสามารถแทนที่กฎเริ่มต้น Haskell forallสำหรับสถานที่ที่จะแทรก

ทำไมคุณถึงต้องการทำเช่นนี้? เนื่องจาก System F เต็มรูปแบบที่ไม่ จำกัด นั้นมีประสิทธิภาพมากและสามารถทำสิ่งดีๆได้มากมาย ตัวอย่างเช่นการซ่อนประเภทและการแยกส่วนสามารถใช้งานได้โดยใช้ประเภทที่มีอันดับสูงกว่า ยกตัวอย่างเช่นฟังก์ชั่นเก่าธรรมดาของประเภทอันดับ -1 ต่อไปนี้ (เพื่อตั้งค่าฉาก):

f :: r.∀a.((a  r)  a  r)  r

กับการใช้งานfโทรครั้งแรกจะต้องเลือกประเภทที่จะใช้สำหรับrและaแล้วจัดหาอาร์กิวเมนต์ชนิดที่ส่งผล คุณสามารถเลือกr = Intและa = String:

f Int String :: ((String  Int)  String  Int)  Int

แต่ตอนนี้เปรียบเทียบกับประเภทที่สูงกว่าต่อไปนี้:

f' :: r.(∀a.(a  r)  a  r)  r

ฟังก์ชันประเภทนี้ทำงานอย่างไร? rดีที่จะใช้มันเป็นครั้งแรกที่คุณระบุประเภทที่จะใช้สำหรับ สมมติว่าเราเลือกInt:

f' Int :: (∀a.(a  Int)  a  Int)  Int

แต่ตอนนี้∀aอยู่ในลูกศรฟังก์ชันดังนั้นคุณไม่สามารถเลือกประเภทที่จะใช้สำหรับa; คุณต้องใช้f' IntกับΛ-function ของประเภทที่เหมาะสม ซึ่งหมายความว่าการนำไปใช้f'เพื่อเลือกประเภทที่จะใช้aไม่ใช่ผู้เรียกf'ใช้ ในทางกลับกันหากไม่มีประเภทที่มีอันดับสูงกว่าผู้โทรจะเลือกประเภทเสมอ

สิ่งนี้มีประโยชน์สำหรับอะไร? สำหรับหลาย ๆ สิ่งที่จริง แต่แนวคิดอย่างหนึ่งก็คือคุณสามารถใช้สิ่งนี้เพื่อสร้างแบบจำลองสิ่งต่างๆเช่นการเขียนโปรแกรมเชิงวัตถุโดยที่ "ออบเจ็กต์" รวมข้อมูลที่ซ่อนอยู่เข้าด้วยกันกับวิธีการบางอย่างที่ทำงานกับข้อมูลที่ซ่อนอยู่ ตัวอย่างเช่นออบเจ็กต์ที่มีสองวิธีการหนึ่งที่ส่งคืนค่าIntและอีกค่าหนึ่งที่ส่งกลับ a Stringสามารถนำไปใช้กับประเภทนี้ได้:

myObject :: r.(∀a.(a  Int, a -> String)  a  r)  r

วิธีนี้ทำงานอย่างไร? aวัตถุที่ถูกนำมาใช้เป็นฟังก์ชั่นที่มีข้อมูลภายในบางส่วนของประเภทที่ซ่อนอยู่ ในการใช้ออบเจ็กต์จริงไคลเอนต์จะส่งผ่านฟังก์ชัน "เรียกกลับ" ที่อ็อบเจ็กต์จะเรียกใช้ด้วยสองวิธี ตัวอย่างเช่น:

myObject String a. λ(length, name):(a  Int, a  String). λobjData:a. name objData)

ที่นี่เรามีโดยทั่วไปเรียกวิธีที่สองของวัตถุหนึ่งที่มีชนิดสำหรับที่ไม่รู้จักa → String aไม่รู้จักmyObjectลูกค้า แต่ไคลเอนต์เหล่านี้รู้จากลายเซ็นว่าพวกเขาจะสามารถใช้ฟังก์ชันอย่างใดอย่างหนึ่งจากสองฟังก์ชันนี้และได้รับIntไฟล์String.

สำหรับตัวอย่างที่เกิดขึ้นจริง Haskell RankNTypesด้านล่างเป็นรหัสที่ผมเขียนเมื่อฉันสอนตัวเอง สิ่งนี้ใช้ประเภทที่เรียกว่าShowBoxซึ่งรวมค่าของประเภทที่ซ่อนไว้เข้าด้วยกันพร้อมกับShowอินสแตนซ์คลาส โปรดทราบว่าในตัวอย่างด้านล่างฉันสร้างรายการของShowBoxองค์ประกอบแรกที่สร้างจากตัวเลขและองค์ประกอบที่สองจากสตริง เนื่องจากประเภทถูกซ่อนโดยใช้ประเภทที่มีอันดับสูงกว่าจึงไม่ละเมิดการตรวจสอบประเภท

{-# 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

PS: สำหรับใครก็ตามที่อ่านบทความนี้และสงสัยว่าExistentialTypesGHC ใช้forallอย่างไรฉันเชื่อว่าสาเหตุนั้นเป็นเพราะมันใช้เทคนิคนี้อยู่เบื้องหลัง


2
ขอบคุณสำหรับคำตอบที่ละเอียดมาก! (ซึ่งบังเอิญในที่สุดก็กระตุ้นให้ฉันเรียนรู้ทฤษฎีประเภทที่เหมาะสมและ System F. )
Aleksandar Dimitrov

5
ถ้าคุณได้existsคำหลักคุณสามารถกำหนดประเภทของการดำรงอยู่ในฐานะ (ตัวอย่าง) ที่data Any = Any (exists a. a) Any :: (exists a. a) -> Anyการใช้∀xP (x) → Q ≡ (∃xP (x)) → Q เราสามารถสรุปได้ว่าAnyอาจมีประเภทforall a. a -> Anyและนั่นคือที่forallมาของคำหลัก ฉันเชื่อว่าประเภทอัตถิภาวนิยมตามที่ GHC ใช้เป็นเพียงประเภทข้อมูลธรรมดาซึ่งมีพจนานุกรมประเภทคลาสที่จำเป็นทั้งหมด (ฉันไม่พบข้อมูลอ้างอิงเพื่อสำรองข้อมูลนี้ขออภัย)
Vitus

2
@Vitus: อัตถิภาวนิยม GHC ไม่ได้เชื่อมโยงกับพจนานุกรมประเภทคลาส คุณสามารถมีdata ApplyBox r = forall a. ApplyBox (a -> r) a; เมื่อคุณจับคู่รูปแบบเพื่อApplyBox f xคุณจะได้รับf :: h -> rและx :: hสำหรับ "ซ่อน" hประเภทที่ ถ้าฉันเข้าใจถูกกรณีพจนานุกรมประเภทคลาสจะถูกแปลเป็นดังนี้: data ShowBox = forall a. Show a => ShowBox aแปลเป็น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

นั่นเป็นคำตอบที่ดีที่ฉันจะต้องใช้เวลาสักพัก ฉันคิดว่าฉันคุ้นเคยกับสิ่งที่เป็นนามธรรม C # generics มากเกินไปดังนั้นฉันจึงใช้สิ่งนั้นมากมายเพื่อให้ได้รับแทนที่จะเข้าใจทฤษฎีจริงๆ
Andrey Shchekin

@sacundim: "พจนานุกรมคลาสพิมพ์ดีดที่จำเป็นทั้งหมด" อาจหมายถึงไม่มีพจนานุกรมเลยถ้าคุณไม่ต้องการ :) ประเด็นของฉันคือ GHC ส่วนใหญ่จะไม่เข้ารหัสประเภทอัตถิภาวนิยมผ่านประเภทที่มีอันดับสูงกว่า (เช่นการเปลี่ยนแปลงที่คุณแนะนำ - ∃xP (x) ~ ∀r (∀xP (x) → r) → r)
Vitus

47

คำตอบของ Luis Casillasให้ข้อมูลที่ดีมากมายเกี่ยวกับความหมายของอันดับ 2 ประเภท แต่ฉันจะขยายในประเด็นหนึ่งที่เขาไม่ได้กล่าวถึง การกำหนดให้อาร์กิวเมนต์เป็นความหลากหลายไม่เพียง แต่อนุญาตให้ใช้กับหลายประเภท นอกจากนี้ยัง จำกัด สิ่งที่ฟังก์ชันนั้นสามารถทำได้กับอาร์กิวเมนต์และวิธีที่จะให้ผลลัพธ์ของมัน นั่นคือทำให้ผู้โทรมีความยืดหยุ่นน้อยลง ทำไมคุณถึงอยากทำเช่นนั้น? ฉันจะเริ่มด้วยตัวอย่างง่ายๆ:

สมมติว่าเรามีประเภทข้อมูล

data Country = BigEnemy | MediumEnemy | PunyEnemy | TradePartner | Ally | BestAlly

และเราต้องการเขียนฟังก์ชัน

f g = launchMissilesAt $ g [BigEnemy, MediumEnemy, PunyEnemy]

ที่รับฟังก์ชั่นที่ควรจะเลือกหนึ่งในองค์ประกอบของรายการที่ได้รับและส่งคืนIOการยิงขีปนาวุธไปที่เป้าหมายนั้น เราสามารถfระบุประเภทง่ายๆ:

f :: ([Country] -> Country) -> IO ()

ปัญหาคือเราอาจเรียกใช้โดยไม่ได้ตั้งใจ

f (\_ -> BestAlly)

แล้วเราจะต้องเจอกับปัญหาใหญ่! ให้fอันดับ 1 ประเภท polymorphic

f :: ([a] -> a) -> IO ()

ไม่ได้ช่วยอะไรเลยเพราะเราเลือกประเภทaเมื่อเราโทรfและเราแค่เชี่ยวชาญCountryและใช้สิ่งที่เป็นอันตรายของเรา\_ -> BestAllyอีกครั้ง วิธีแก้ปัญหาคือการใช้อันดับ 2 ประเภท:

f :: (forall a . [a] -> a) -> IO ()

ตอนนี้ฟังก์ชั่นที่เราส่งผ่านจำเป็นต้องเป็น polymorphic ดังนั้น\_ -> BestAllyจะไม่พิมพ์ check! ในความเป็นจริงไม่มีฟังก์ชั่นที่ส่งคืนองค์ประกอบที่ไม่อยู่ในรายการที่ได้รับจะพิมพ์ดีด (แม้ว่าบางฟังก์ชันที่เข้าสู่ลูปที่ไม่มีที่สิ้นสุดหรือสร้างข้อผิดพลาดและไม่ส่งคืนก็จะทำเช่นนั้น)

ข้างต้นเป็นสิ่งที่สร้างขึ้นแน่นอน แต่รูปแบบของเทคนิคนี้เป็นกุญแจสำคัญในการทำให้โมนาดSTปลอดภัย


18

ประเภทที่มีอันดับสูงกว่าไม่ได้แปลกใหม่เหมือนกับคำตอบอื่น ๆ เชื่อหรือไม่ว่าภาษาเชิงวัตถุจำนวนมาก (รวมถึง Java และ C #!) มีคุณสมบัติเหล่านี้ (แน่นอนว่าไม่มีใครในชุมชนเหล่านั้นรู้จักพวกเขาด้วยชื่อที่ฟังดูน่ากลัว "ประเภทที่มีอันดับสูงกว่า")

ตัวอย่างที่ฉันจะให้คือการใช้รูปแบบผู้เยี่ยมชมในตำราเรียนซึ่งฉันใช้ตลอดเวลาในการทำงานประจำวัน คำตอบนี้ไม่ได้มีไว้เพื่อเป็นการแนะนำรูปแบบผู้เยี่ยมชม ความรู้ที่เป็นได้อย่างง่ายดาย พร้อมใช้งาน อื่น ๆ

ในแอปพลิเคชัน HR ในจินตนาการที่ร้ายแรงนี้เราต้องการดำเนินการกับพนักงานที่อาจเป็นพนักงานประจำเต็มเวลาหรือผู้รับเหมาชั่วคราว ตัวแปรที่ฉันต้องการของรูปแบบผู้เยี่ยมชม (และแน่นอนว่าเป็นRankNTypesพารามิเตอร์ที่เกี่ยวข้องกับ) เป็นพารามิเตอร์ประเภทการส่งคืนของผู้เยี่ยมชม

interface IEmployeeVisitor<T>
{
    T Visit(PermanentEmployee e);
    T Visit(Contractor c);
}

class XmlVisitor : IEmployeeVisitor<string> { /* ... */ }
class PaymentCalculator : IEmployeeVisitor<int> { /* ... */ }

ประเด็นก็คือผู้เยี่ยมชมจำนวนหนึ่งที่มีประเภทผลตอบแทนต่างกันทั้งหมดสามารถดำเนินการกับข้อมูลเดียวกันได้ ซึ่งหมายความว่าIEmployeeจะต้องไม่แสดงความคิดเห็นว่าTควรจะเป็นอย่างไร

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);
    }
}

ฉันต้องการดึงดูดความสนใจของคุณไปยังประเภทต่างๆ สังเกตว่าIEmployeeVisitorประเภทผลตอบแทนในระดับสากลในขณะที่IEmployeeหาปริมาณภายในAcceptวิธีการของมันกล่าวคือในอันดับที่สูงกว่า แปล clunkily จาก C # เป็น Haskell:

data IEmployeeVisitor r = IEmployeeVisitor {
    visitPermanent :: PermanentEmployee -> r,
    visitContractor :: Contractor -> r
}

newtype IEmployee = IEmployee {
    accept :: forall r. IEmployeeVisitor r -> r
}

คุณมีแล้ว ประเภทอันดับที่สูงกว่าจะแสดงใน C # เมื่อคุณเขียนประเภทที่มีวิธีการทั่วไป


1
ฉันอยากทราบว่ามีใครเขียนเกี่ยวกับการสนับสนุน C # / Java / Blub สำหรับประเภทที่มีอันดับสูงกว่านี้หรือไม่ หากคุณผู้อ่านที่รักรู้จักแหล่งข้อมูลดังกล่าวโปรดส่งมาทางฉัน!
Benjamin Hodgson


-2

สำหรับผู้ที่คุ้นเคยกับภาษาเชิงวัตถุฟังก์ชันที่มีอันดับสูงกว่าเป็นเพียงฟังก์ชันทั่วไปที่คาดหวังว่าอาร์กิวเมนต์เป็นฟังก์ชันทั่วไปอีกฟังก์ชันหนึ่ง

เช่นใน TypeScript คุณสามารถเขียน:

type WithId<T> = T & { id: number }
type Identifier = <T>(obj: T) => WithId<T>
type Identify = <TObj>(obj: TObj, f: Identifier) => WithId<TObj>

ดูว่าประเภทฟังก์ชันทั่วไปIdentifyต้องการฟังก์ชันทั่วไปของประเภทIdentifierอย่างไร? สิ่งนี้ทำให้Identifyฟังก์ชันอันดับสูงขึ้น


สิ่งนี้เพิ่มอะไรให้กับคำตอบของ sepp2k?
dfeuer

หรือของเบนจามินฮอดจ์สันสำหรับเรื่องนั้น?
dfeuer

1
ฉันคิดว่าคุณพลาดประเด็นของฮอดจ์สัน Acceptมีประเภท polymorphic อันดับ 1 แต่เป็นวิธีการIEmployeeซึ่งตัวเองเป็นอันดับที่ 2 ถ้ามีคนให้ฉันIEmployeeฉันสามารถเปิดและใช้Acceptวิธีการใดก็ได้
dfeuer

1
ตัวอย่างของคุณยังเป็นอันดับที่ 2 ตามVisiteeคลาสที่คุณแนะนำ ฟังก์ชันf :: Visitee e => T eคือ (เมื่อสิ่งที่คลาสถูกออกแบบมา) โดยพื้นฐานf :: (forall r. e -> Visitor e r -> r) -> T eแล้ว Haskell 2010 ช่วยให้คุณหลีกหนีจากความหลากหลายของอันดับ 2 ที่ จำกัด โดยใช้คลาสเช่นนั้น
dfeuer

1
คุณไม่สามารถลอยออกมาforallในตัวอย่างของฉันได้ ฉันไม่ได้มีการอ้างอิงมือปิด แต่คุณดีอาจพบบางสิ่งบางอย่างใน"เศษเรียนประเภทของคุณ" ความหลากหลายที่มีอันดับสูงกว่าสามารถทำให้เกิดปัญหาการตรวจสอบประเภทได้ แต่การเรียงลำดับที่ จำกัด โดยนัยในระบบคลาสนั้นใช้ได้
dfeuer
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.