ชี้แจงเกี่ยวกับประเภทที่มีอยู่ใน Haskell


10

ฉันพยายามที่จะเข้าใจประเภท Existential ใน Haskell และเจอ PDF http://www.ii.uni.wroc.pl/~dabi/courses/ZPF15/rlasocha/prezentacja.pdf

โปรดแก้ไขความเข้าใจด้านล่างที่ฉันมีจนถึงตอนนี้

  • ประเภทที่มีอยู่ดูเหมือนจะไม่สนใจในประเภทที่มีอยู่ แต่การจับคู่รูปแบบพวกเขาบอกว่ามีประเภทบางประเภทที่เราไม่ทราบว่าเป็นประเภทใดจนกระทั่งถึง & ยกเว้นว่าเราใช้ Typeable หรือ Data
  • เราใช้มันเมื่อเราต้องการซ่อนประเภท (เช่น: สำหรับรายการที่ต่างกัน) หรือเราไม่ทราบว่าประเภทใดในเวลารวบรวม
  • GADT's ให้ไวยากรณ์ที่ชัดเจนและดีกว่าที่จะใช้รหัสประเภทอัตถิภาวนิยมโดยการให้นัยforall' s

ข้อสงสัยของฉัน

  • ในหน้า 20 ของ PDF ข้างต้นมีการกล่าวถึงสำหรับโค้ดด้านล่างว่าเป็นไปไม่ได้ที่ Function ต้องการบัฟเฟอร์เฉพาะ ทำไมถึงเป็นเช่นนั้น เมื่อฉันร่างฟังก์ชั่นฉันรู้ว่าบัฟเฟอร์ชนิดใดฉันจะใช้แม้จะไม่รู้ว่าข้อมูลที่ฉันจะใส่เข้าไปนั้น มีอะไรผิดปกติในการมี:: Worker MemoryBuffer Intถ้าพวกเขาต้องการที่จะทำให้นามธรรมเหนือบัฟเฟอร์พวกเขาสามารถมีประเภท Sum data Buffer = MemoryBuffer | NetBuffer | RandomBufferและมีประเภทเช่น:: Worker Buffer Int
data Worker x = forall b. Buffer b => Worker {buffer :: b, input :: x}
data MemoryBuffer = MemoryBuffer

memoryWorker = Worker MemoryBuffer (1 :: Int)
memoryWorker :: Worker Int
  • ในฐานะที่เป็น Haskell เป็นภาษาแบบเต็มรูปแบบลบเช่น C แล้วมันจะรู้ได้อย่างไรว่า Runtime ซึ่งฟังก์ชั่นการโทร มันเป็นเหมือนเราจะเก็บรักษาข้อมูลเล็กน้อยและส่งผ่าน V-Table ขนาดใหญ่ของฟังก์ชั่นและที่รันไทม์มันจะคิดออกจาก V-Table? ถ้าเป็นเช่นนั้นแล้วมันจะเก็บข้อมูลประเภทใด?

คำตอบ:


8

GADT จัดเตรียมไวยากรณ์ที่ชัดเจนและดีกว่าให้กับโค้ดโดยใช้ประเภทที่มีอยู่โดยจัดเตรียมให้โดยนัยสำหรับ forall

ฉันคิดว่ามีข้อตกลงทั่วไปว่าไวยากรณ์ GADT ดีกว่า ฉันจะไม่บอกว่าเป็นเพราะ GADT ให้การบอกทางโดยปริยาย แต่เนื่องจากไวยากรณ์ดั้งเดิมที่เปิดใช้งานกับExistentialQuantificationส่วนขยายนั้นอาจทำให้สับสน / ทำให้เข้าใจผิด แน่นอนว่าไวยากรณ์นั้นมีลักษณะดังนี้:

data SomeType = forall a. SomeType a

หรือมีข้อ จำกัด :

data SomeShowableType = forall a. Show a => SomeShowableType a

และฉันคิดว่าฉันทามติคือการใช้คำหลักforallที่นี่ทำให้ประเภทสับสนได้ง่ายกับประเภทที่แตกต่างกันโดยสิ้นเชิง:

data AnyType = AnyType (forall a. a)    -- need RankNTypes extension

ไวยากรณ์ที่ดีกว่าอาจใช้existsคำหลักแยกต่างหากดังนั้นคุณจะต้องเขียน:

data SomeType = SomeType (exists a. a)   -- not valid GHC syntax

ไวยากรณ์ของ GADT ไม่ว่าจะใช้กับแบบทางอ้อมหรือแบบชัดแจ้งforallนั้นมีความเหมือนกันมากกว่าในทุกประเภทและดูเหมือนจะเข้าใจได้ง่ายขึ้น แม้ว่าจะมีforallคำจำกัดความที่ชัดเจน แต่คำจำกัดความต่อไปนี้ก็ยังข้ามความคิดที่ว่าคุณสามารถรับค่าได้ทุกประเภทaและวางไว้ใน monomorphic SomeType':

data SomeType' where
    SomeType' :: forall a. (a -> SomeType')   -- parentheses optional

และง่ายต่อการดูและเข้าใจความแตกต่างระหว่างประเภทนั้นและ:

data AnyType' where
    AnyType' :: (forall a. a) -> AnyType'

ประเภทที่มีอยู่ดูเหมือนจะไม่สนใจในประเภทที่มีอยู่ แต่การจับคู่รูปแบบพวกเขาบอกว่ามีประเภทบางประเภทที่เราไม่ทราบว่าเป็นประเภทใดจนกระทั่งถึง & ยกเว้นว่าเราใช้ Typeable หรือ Data

เราใช้มันเมื่อเราต้องการซ่อนประเภท (เช่น: สำหรับรายการที่ต่างกัน) หรือเราไม่ทราบว่าประเภทใดในเวลารวบรวม

ฉันเดาว่าสิ่งเหล่านี้ไม่ไกลเกินไปแม้ว่าคุณไม่จำเป็นต้องใช้TypeableหรือDataใช้ชนิดที่มีอยู่ ฉันคิดว่ามันจะแม่นยำกว่าถ้าบอกว่าประเภทที่มีอยู่ให้ "กล่อง" ที่พิมพ์ได้ดีรอบประเภทที่ไม่ระบุ กล่องจะ "ซ่อน" ประเภทในแง่ที่ช่วยให้คุณสร้างรายการที่แตกต่างกันของกล่องดังกล่าวโดยไม่สนใจประเภทที่มีอยู่ ปรากฎว่าการดำรงอยู่ที่ไม่มีข้อ จำกัด เช่นเดียวกับSomeType'ข้างบนนั้นค่อนข้างไร้ประโยชน์ แต่เป็นข้อ จำกัด ประเภท:

data SomeShowableType' where
    SomeShowableType' :: forall a. (Show a) => a -> SomeShowableType'

ช่วยให้คุณสามารถจับคู่รูปแบบเพื่อดูภายใน "กล่อง" และทำให้สิ่งอำนวยความสะดวกชั้นเรียนมีให้บริการ

showIt :: SomeShowableType' -> String
showIt (SomeShowableType' x) = show x

โปรดทราบว่าการทำงานนี้สำหรับการเรียนประเภทใด ๆ ที่ไม่เพียงหรือTypeableData

สำหรับความสับสนของคุณเกี่ยวกับหน้า 20 ของเด็คสไลด์ผู้เขียนบอกว่ามันเป็นไปไม่ได้สำหรับฟังก์ชั่นที่มีอยู่ Workerเพื่อเรียกร้องให้WorkerมีBufferอินสแตนซ์เฉพาะ คุณสามารถเขียนฟังก์ชั่นเพื่อสร้างประเภทการWorkerใช้งานBufferเช่นMemoryBuffer:

class Buffer b where
  output :: String -> b -> IO ()
data Worker x = forall b. Buffer b => Worker {buffer :: b, input :: x}
data MemoryBuffer = MemoryBuffer
instance Buffer MemoryBuffer

memoryWorker = Worker MemoryBuffer (1 :: Int)
memoryWorker :: Worker Int

แต่ถ้าคุณเขียนฟังก์ชั่นที่รับWorkerอาร์กิวเมนต์มันจะสามารถใช้Bufferสิ่งอำนวยความสะดวกระดับประเภททั่วไปเท่านั้น(เช่นฟังก์ชั่นoutput):

doWork :: Worker Int -> IO ()
doWork (Worker b x) = output (show x) b

ไม่สามารถลองใช้ความต้องการที่bเป็นบัฟเฟอร์ชนิดใดประเภทหนึ่งได้แม้ผ่านการจับคู่รูปแบบ:

doWorkBroken :: Worker Int -> IO ()
doWorkBroken (Worker b x) = case b of
  MemoryBuffer -> error "try this"       -- type error
  _            -> error "try that"

ในที่สุดข้อมูลรันไทม์เกี่ยวกับประเภทที่มีอยู่จะมีให้ผ่านอาร์กิวเมนต์ "พจนานุกรม" โดยนัยสำหรับประเภทข้อมูลที่เกี่ยวข้อง Workerประเภทข้างต้นใน addtion จะมีฟิลด์สำหรับบัฟเฟอร์และใส่นอกจากนี้ยังมีสนามนัยมองไม่เห็นว่าจุดที่จะBufferพจนานุกรม (คล้ายตาราง v แม้ว่ามันจะใหญ่แทบจะไม่เป็นมันก็มีตัวชี้ไปยังที่เหมาะสมoutputฟังก์ชั่น)

ภายในคลาสชนิดBufferถูกแสดงเป็นชนิดข้อมูลที่มีเขตข้อมูลฟังก์ชันและอินสแตนซ์คือ "พจนานุกรม" ประเภทนี้:

data Buffer' b = Buffer' { output' :: String -> b -> IO () }

dBuffer_MemoryBuffer :: Buffer' MemoryBuffer
dBuffer_MemoryBuffer = Buffer' { output' = undefined }

ประเภทที่มีอยู่มีฟิลด์ที่ซ่อนอยู่สำหรับพจนานุกรมนี้:

data Worker' x = forall b. Worker' { dBuffer :: Buffer' b, buffer' :: b, input' :: x }

และฟังก์ชั่นเช่นนี้doWorkที่ทำงานกับWorker'ค่าที่มีอยู่จะถูกนำไปใช้เป็น:

doWork' :: Worker' Int -> IO ()
doWork' (Worker' dBuf b x) = output' dBuf (show x) b

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


มีอยู่จริงเป็นอันดับ 1 สำหรับการประกาศข้อมูล? มีอยู่เป็นวิธีจัดการฟังก์ชั่นเสมือนจริงใน Haskell เหมือนในภาษา OOP ใด ๆ ?
Pawan Kumar

1
ฉันอาจไม่ได้เรียกAnyTypeประเภทอันดับ 2 นั่นเป็นเพียงความสับสนและฉันได้ลบมัน ตัวสร้างAnyTypeทำหน้าที่เหมือนฟังก์ชั่นอันดับ 2 และตัวสร้างSomeTypeทำหน้าที่จัดอันดับฟังก์ชั่น 1 (เช่นเดียวกับประเภทที่ไม่มีอยู่ส่วนใหญ่) แต่นั่นไม่ใช่ลักษณะที่เป็นประโยชน์มาก หากมีสิ่งใดสิ่งที่ทำให้สิ่งเหล่านี้น่าสนใจก็คือพวกมันอยู่ในอันดับที่ 0 (กล่าวคือไม่ได้วัดปริมาณของตัวแปรประเภทและ monomorphic) ด้วยตนเองถึงแม้ว่าพวกเขาจะ "บรรจุ" ประเภทที่ระบุ
KA Buhr

1
ประเภทคลาส (และโดยเฉพาะอย่างยิ่งฟังก์ชั่นวิธีการของพวกเขา) แทนที่จะเป็นประเภทที่มีอยู่น่าจะเป็น Haskell โดยตรงที่สุดเทียบเท่ากับฟังก์ชั่นเสมือนจริง ในความหมายทางเทคนิคคลาสและวัตถุของภาษา OOP สามารถดูได้ว่าเป็นประเภทและค่าที่มีอยู่จริง แต่ในทางปฏิบัติมักจะมีวิธีที่ดีกว่าในการใช้รูปแบบ polymorphism ใน Haskell ใน OOP "virtual function" ในรูปแบบที่หลากหลายกว่า คลาสประเภทและ / หรือตัวแปรหลากหลาย
KA Buhr

4

ในหน้า 20 ของ PDF ข้างต้นมีการกล่าวถึงโค้ดด้านล่างว่าเป็นไปไม่ได้ที่ Function ต้องการบัฟเฟอร์เฉพาะ ทำไมถึงเป็นเช่นนั้น?

เพราะWorkerตามที่กำหนดจะรับเพียงหนึ่งอาร์กิวเมนต์เท่านั้นซึ่งเป็นประเภทของฟิลด์ "อินพุต" (ตัวแปรชนิดx) เช่นWorker Intเป็นประเภท ตัวแปร type bแทนไม่ใช่พารามิเตอร์Workerแต่เป็นประเภท "ตัวแปรท้องถิ่น" เพื่อพูด ไม่สามารถส่งผ่านได้เหมือนในWorker Int String- ซึ่งจะทำให้เกิดข้อผิดพลาดประเภท

หากเรากำหนดไว้:

data Worker x b = Worker {buffer :: b, input :: x}

จากนั้นWorker Int Stringก็ใช้งานได้ แต่ประเภทนั้นไม่มีอยู่จริงอีกต่อไป - ตอนนี้เราต้องผ่านประเภทบัฟเฟอร์เช่นกัน

ในฐานะที่เป็น Haskell เป็นภาษาแบบเต็มรูปแบบลบเช่น C แล้วมันจะรู้ได้อย่างไรว่า Runtime ซึ่งฟังก์ชั่นการโทร มันเป็นเหมือนเราจะเก็บรักษาข้อมูลเล็กน้อยและส่งผ่าน V-Table ขนาดใหญ่ของฟังก์ชั่นและที่รันไทม์มันจะคิดออกจาก V-Table? ถ้าเป็นเช่นนั้นแล้วมันจะเก็บข้อมูลประเภทใด?

สิ่งนี้ถูกต้องคร่าวๆ สั้น ๆ ใส่ทุกครั้งที่คุณใช้คอนสตรัคWorker, GHC อนุมานbประเภทจากการขัดแย้งของแล้วค้นหาอินสแตนซ์Worker Buffer bหากพบว่า GHC มีตัวชี้เพิ่มเติมไปยังอินสแตนซ์ในวัตถุ ในรูปแบบที่ง่ายที่สุดสิ่งนี้ไม่แตกต่างจาก "ตัวชี้ไปยัง vtable" ซึ่งถูกเพิ่มลงในแต่ละวัตถุใน OOP เมื่อมีฟังก์ชั่นเสมือนอยู่

ในกรณีทั่วไปอาจมีความซับซ้อนมากกว่าเดิม คอมไพเลอร์อาจใช้การเป็นตัวแทนที่แตกต่างกันและเพิ่มพอยน์เตอร์มากขึ้นแทนที่จะเป็นพอยน์เตอร์เดียว (พูดโดยการเพิ่มพอยน์เตอร์ไปยังเมธอดอินสแตนซ์ทั้งหมดโดยตรง) หากความเร็วนั้นเพิ่มโค้ด นอกจากนี้บางครั้งคอมไพเลอร์จำเป็นต้องใช้หลายอินสแตนซ์เพื่อตอบสนองข้อ จำกัด เช่นหากเราต้องการเก็บอินสแตนซ์สำหรับEq [Int]... ก็ไม่มีหนึ่ง แต่สอง: หนึ่งสำหรับIntและหนึ่งสำหรับรายการและทั้งสองจะต้องรวมกัน (ในเวลาทำงาน, การ จำกัด การเพิ่มประสิทธิภาพ)

เป็นการยากที่จะคาดเดาสิ่งที่ GHC ทำในแต่ละกรณี: ขึ้นอยู่กับการปรับแต่งที่เหมาะสมซึ่งอาจหรือไม่ก่อให้เกิด

คุณสามารถลองใช้ googling สำหรับการใช้งาน "ประเภทพจนานุกรม" เพื่อเรียนรู้เพิ่มเติมเกี่ยวกับสิ่งที่เกิดขึ้น นอกจากนี้คุณยังสามารถขอให้ GHC พิมพ์ Core ที่ได้รับการปรับปรุงภายในด้วย-ddump-simplและสังเกตพจนานุกรมที่กำลังสร้างจัดเก็บและส่งผ่าน ฉันต้องเตือนคุณ: หลักอยู่ในระดับต่ำและอ่านได้ยากในตอนแรก

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.