ฉันไม่ค่อยเชี่ยวชาญเรื่อง Haskell ดังนั้นนี่อาจเป็นคำถามที่ง่ายมาก
Rank2Typesแก้ข้อ จำกัด ด้านภาษาอะไรบ้าง? ฟังก์ชั่นใน Haskell ไม่รองรับอาร์กิวเมนต์ polymorphic แล้วหรือ?
ฉันไม่ค่อยเชี่ยวชาญเรื่อง Haskell ดังนั้นนี่อาจเป็นคำถามที่ง่ายมาก
Rank2Typesแก้ข้อ จำกัด ด้านภาษาอะไรบ้าง? ฟังก์ชั่นใน Haskell ไม่รองรับอาร์กิวเมนต์ polymorphic แล้วหรือ?
คำตอบ:
ฟังก์ชั่นใน 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 ได้
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
เป็นอย่างไร
เป็นเรื่องยากที่จะเข้าใจความแตกต่างที่มีอันดับสูงกว่าเว้นแต่คุณจะศึกษาSystem Fโดยตรงเนื่องจาก Haskell ได้รับการออกแบบมาเพื่อซ่อนรายละเอียดของสิ่งนั้นจากคุณเพื่อความเรียบง่าย
แต่โดยพื้นฐานแล้วแนวคิดคร่าวๆก็คือประเภทของโพลีมอร์ฟิกไม่ได้มีa -> b
รูปแบบที่พวกเขาทำในฮัสเคลล์ ในความเป็นจริงพวกมันมีลักษณะเช่นนี้เสมอโดยมีตัวระบุปริมาณที่ชัดเจน:
id :: ∀a.a → a
id = Λt.λx:t.x
หากคุณไม่ทราบสัญลักษณ์ "∀" แสดงว่า "สำหรับทั้งหมด" ∀x.dog(x)
หมายความว่า "สำหรับ x ทั้งหมด x คือสุนัข" "Λ" คือตัวพิมพ์ใหญ่แลมบ์ดาใช้สำหรับการแยกพารามิเตอร์ประเภท สิ่งที่บรรทัดที่สองกล่าวคือ id คือฟังก์ชันที่รับประเภทt
แล้วส่งกลับฟังก์ชันที่พาราเมตไตรด์ตามประเภทนั้น
คุณจะเห็นว่าในระบบ F คุณไม่สามารถใช้ฟังก์ชันแบบนั้นid
กับค่าได้ทันที ก่อนอื่นคุณต้องใช้Λ-function กับชนิดเพื่อให้ได้λ-function ที่คุณใช้กับค่า ตัวอย่างเช่น:
(Λt.λx: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: สำหรับใครก็ตามที่อ่านบทความนี้และสงสัยว่าExistentialTypes
GHC ใช้forall
อย่างไรฉันเชื่อว่าสาเหตุนั้นเป็นเพราะมันใช้เทคนิคนี้อยู่เบื้องหลัง
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 ใช้เป็นเพียงประเภทข้อมูลธรรมดาซึ่งมีพจนานุกรมประเภทคลาสที่จำเป็นทั้งหมด (ฉันไม่พบข้อมูลอ้างอิงเพื่อสำรองข้อมูลนี้ขออภัย)
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ให้ข้อมูลที่ดีมากมายเกี่ยวกับความหมายของอันดับ 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
ปลอดภัย
ประเภทที่มีอันดับสูงกว่าไม่ได้แปลกใหม่เหมือนกับคำตอบอื่น ๆ เชื่อหรือไม่ว่าภาษาเชิงวัตถุจำนวนมาก (รวมถึง 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 # เมื่อคุณเขียนประเภทที่มีวิธีการทั่วไป
สไลด์จากหลักสูตร Haskell ไบรอันซัลลิแวนที่สแตนฟอRank2Types
ช่วยให้ฉันเข้าใจ
สำหรับผู้ที่คุ้นเคยกับภาษาเชิงวัตถุฟังก์ชันที่มีอันดับสูงกว่าเป็นเพียงฟังก์ชันทั่วไปที่คาดหวังว่าอาร์กิวเมนต์เป็นฟังก์ชันทั่วไปอีกฟังก์ชันหนึ่ง
เช่นใน 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
ฟังก์ชันอันดับสูงขึ้น
Accept
มีประเภท polymorphic อันดับ 1 แต่เป็นวิธีการIEmployee
ซึ่งตัวเองเป็นอันดับที่ 2 ถ้ามีคนให้ฉันIEmployee
ฉันสามารถเปิดและใช้Accept
วิธีการใดก็ได้
Visitee
คลาสที่คุณแนะนำ ฟังก์ชันf :: Visitee e => T e
คือ (เมื่อสิ่งที่คลาสถูกออกแบบมา) โดยพื้นฐานf :: (forall r. e -> Visitor e r -> r) -> T e
แล้ว Haskell 2010 ช่วยให้คุณหลีกหนีจากความหลากหลายของอันดับ 2 ที่ จำกัด โดยใช้คลาสเช่นนั้น
forall
ในตัวอย่างของฉันได้ ฉันไม่ได้มีการอ้างอิงมือปิด แต่คุณดีอาจพบบางสิ่งบางอย่างใน"เศษเรียนประเภทของคุณ" ความหลากหลายที่มีอันดับสูงกว่าสามารถทำให้เกิดปัญหาการตรวจสอบประเภทได้ แต่การเรียงลำดับที่ จำกัด โดยนัยในระบบคลาสนั้นใช้ได้