ฉันไม่ค่อยเชี่ยวชาญเรื่อง 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: สำหรับใครก็ตามที่อ่านบทความนี้และสงสัยว่าExistentialTypesGHC ใช้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ในตัวอย่างของฉันได้ ฉันไม่ได้มีการอ้างอิงมือปิด แต่คุณดีอาจพบบางสิ่งบางอย่างใน"เศษเรียนประเภทของคุณ" ความหลากหลายที่มีอันดับสูงกว่าสามารถทำให้เกิดปัญหาการตรวจสอบประเภทได้ แต่การเรียงลำดับที่ จำกัด โดยนัยในระบบคลาสนั้นใช้ได้