การตรวจสอบประเภทและประเภทการเรียกซ้ำ (การเขียน Y combinator ใน Haskell / Ocaml)


21

เมื่ออธิบายผู้ประสานงาน Y ในบริบทของ Haskell มักจะกล่าวว่าการนำไปใช้งานแบบตรงไปตรงมาจะไม่ตรวจสอบประเภทใน Haskell เนื่องจากเป็นแบบเรียกซ้ำ

ตัวอย่างเช่นจากRosettacode :

The obvious definition of the Y combinator in Haskell canot be used
because it contains an infinite recursive type (a = a -> b). Defining
a data type (Mu) allows this recursion to be broken.

newtype Mu a = Roll { unroll :: Mu a -> a }

fix :: (a -> a) -> a
fix = \f -> (\x -> f (unroll x x)) $ Roll (\x -> f (unroll x x))

และแน่นอนคำจำกัดความ“ ชัดเจน” ไม่ได้ตรวจสอบ

λ> let fix f g = (\x -> \a -> f (x x) a) (\x -> \a -> f (x x) a) g

<interactive>:10:33:
    Occurs check: cannot construct the infinite type:
      t2 = t2 -> t0 -> t1
    Expected type: t2 -> t0 -> t1
      Actual type: (t2 -> t0 -> t1) -> t0 -> t1
    In the first argument of `x', namely `x'
    In the first argument of `f', namely `(x x)'
    In the expression: f (x x) a

<interactive>:10:57:
    Occurs check: cannot construct the infinite type:
      t2 = t2 -> t0 -> t1
    In the first argument of `x', namely `x'
    In the first argument of `f', namely `(x x)'
    In the expression: f (x x) a
(0.01 secs, 1033328 bytes)

ข้อ จำกัด เดียวกันมีอยู่ใน Ocaml:

utop # let fix f g = (fun x a -> f (x x) a) (fun x a -> f (x x) a) g;;
Error: This expression has type 'a -> 'b but an expression was expected of type 'a                                    
       The type variable 'a occurs inside 'a -> 'b

อย่างไรก็ตามใน Ocaml เราสามารถอนุญาตประเภทเรียกซ้ำโดยผ่าน-rectypesสวิตช์:

   -rectypes
          Allow  arbitrary  recursive  types  during type-checking.  By default, only recursive
          types where the recursion goes through an object type are supported.

โดยการใช้-rectypesงานทุกอย่าง:

utop # let fix f g = (fun x a -> f (x x) a) (fun x a -> f (x x) a) g;;
val fix : (('a -> 'b) -> 'a -> 'b) -> 'a -> 'b = <fun>
utop # let fact_improver partial n = if n = 0 then 1 else n*partial (n-1);;
val fact_improver : (int -> int) -> int -> int = <fun>
utop # (fix fact_improver) 5;;
- : int = 120

อยากรู้เกี่ยวกับระบบการพิมพ์และการอนุมานแบบนี้ทำให้เกิดคำถามบางอย่างที่ฉันยังไม่สามารถตอบได้

  • ขั้นแรกให้ตัวตรวจสอบประเภทมากับประเภทt2 = t2 -> t0 -> t1อย่างไร เมื่อมากับประเภทนั้นฉันเดาว่าปัญหาคือประเภท ( t2) อ้างถึงตัวเองทางด้านขวาหรือไม่
  • ข้อที่สองและอาจจะน่าสนใจที่สุดอะไรคือเหตุผลที่ทำให้ระบบประเภท Haskell / Ocaml ไม่อนุญาตให้ทำเช่นนี้? ผมคิดว่ามีเป็นเหตุผลที่ดีตั้งแต่ Ocaml ยังจะไม่อนุญาตให้ได้โดยเริ่มต้นถึงแม้ว่ามันจะสามารถจัดการกับประเภท recursive ถ้าได้รับ-rectypesสวิทช์

หากสิ่งเหล่านี้เป็นหัวข้อใหญ่จริง ๆ ฉันขอขอบคุณพอยน์เตอร์สำหรับวรรณกรรมที่เกี่ยวข้อง

คำตอบ:


16

ก่อนข้อผิดพลาด GHC

GHC พยายามรวมข้อ จำกัด บางอย่างไว้ด้วยxก่อนอื่นเราใช้มันเป็นฟังก์ชั่น

x :: a -> b

ต่อไปเราจะใช้เป็นค่าในฟังก์ชั่นนั้น

x :: a

และในที่สุดเราก็รวมมันเข้ากับการแสดงออกของอาร์กิวเมนต์ดั้งเดิมดังนั้น

x :: (a -> b) -> c -> d

ตอนนี้x xกลายเป็นความพยายามที่จะรวมกันt2 -> t1 -> t0แต่เราไม่สามารถรวมกันนี้เพราะจะต้องรวมt2อาร์กิวเมนต์แรกของการมีx xดังนั้นข้อความผิดพลาดของเรา

ถัดไปทำไมไม่ใช่ประเภทการเรียกซ้ำทั่วไป จุดแรกที่ควรสังเกตคือความแตกต่างระหว่าง equi และ iso recursive

  • equi-recursive เป็นสิ่งที่คุณคาดหวัง mu X . Typeจะเทียบเท่ากับการขยายหรือพับมันโดยพลการ
  • ประเภท iso-recursive จัดเตรียมคู่ของโอเปอเรเตอร์foldและunfoldพับและคลายคำจำกัดความของชนิดเรียกซ้ำ

ตอนนี้ประเภท equi-recursive เหมาะอย่างยิ่ง แต่ยากที่จะเข้าใจผิดในระบบประเภทที่ซับซ้อน มันสามารถทำการตรวจสอบชนิด undecidable จริง ๆ ฉันไม่คุ้นเคยกับรายละเอียดทุกอย่างของระบบประเภทของ OCaml แต่ประเภท equirecursive ทั้งหมดใน Haskell สามารถทำให้ typechecker วนซ้ำโดยพลการพยายามที่จะรวมประเภทโดยค่าเริ่มต้น Haskell ทำให้แน่ใจว่าการตรวจสอบประเภทนั้นสิ้นสุดลง ยิ่งไปกว่านั้นใน Haskell คำพ้องความหมายของประเภทเป็นใบ้ประเภทเรียกซ้ำที่มีประโยชน์ที่สุดจะถูกกำหนดเช่นtype T = T -> ()อย่างไรก็ตามจะมีการแทรกเกือบจะทันทีใน Haskell แต่คุณไม่สามารถแทรกประเภทเรียกซ้ำได้มันไม่มีที่สิ้นสุด! ดังนั้นประเภทการเรียกซ้ำใน Haskell จึงต้องการการยกเครื่องครั้งใหญ่สำหรับวิธีการจัดการคำพ้องความหมายอาจไม่คุ้มค่ากับความพยายามที่จะใส่แม้จะเป็นการขยายภาษา

ประเภท Iso-recursive เป็นบิตของความเจ็บปวดที่จะใช้คุณมากหรือน้อยต้องบอกตัวตรวจสอบชนิดอย่างชัดเจนถึงวิธีการพับและแฉประเภทของคุณทำให้โปรแกรมของคุณซับซ้อนมากขึ้นในการอ่านและเขียน

อย่างไรก็ตามสิ่งนี้คล้ายกับสิ่งที่คุณทำกับMuประเภทของคุณ Rollพับและunrollกางออก ที่จริงแล้วเรามีการอบไอโซแบบเรียกซ้ำ (recursive type) อย่างไรก็ตามประเภท equi-recursive นั้นซับซ้อนเกินไปดังนั้นระบบเช่น OCaml และ Haskell บังคับให้คุณผ่านการกำเริบผ่านจุดตรึงระดับประเภท

ตอนนี้ถ้าคุณสนใจฉันจะแนะนำประเภทและภาษาการเขียนโปรแกรม สำเนาของฉันกำลังนั่งอยู่บนตักขณะที่ฉันกำลังเขียนสิ่งนี้เพื่อให้แน่ใจว่าฉันมีคำศัพท์ที่ถูกต้องแล้ว :)


โดยเฉพาะอย่างยิ่งในบทที่ 21 ให้สัญชาตญาณที่ดีสำหรับการเหนี่ยวนำการคิดแบบเหรียญและแบบเรียกซ้ำ
Daniel Gratzer

ขอขอบคุณ! นี่มันช่างน่าหลงใหลจริงๆ ฉันกำลังอ่าน TAPL และฉันดีใจที่ได้ยินว่าหนังสือเล่มนี้จะกล่าวถึงในภายหลัง
เบต้า

@beta Yep, TAPL และเป็นพี่ใหญ่หัวข้อขั้นสูงในประเภทและภาษาการเขียนโปรแกรมเป็นแหล่งข้อมูลที่ยอดเยี่ยม
Daniel Gratzer

2

ใน OCaml คุณต้องผ่าน-rectypesเป็นพารามิเตอร์ไปยังคอมไพเลอร์ (หรือป้อน#rectypes;;ในระดับบนสุด) โดยประมาณการพูดสิ่งนี้จะปิด "การตรวจสอบที่เกิดขึ้น" ในระหว่างการรวม สถานการณ์The type variable 'a occurs inside 'a -> 'bจะไม่เป็นปัญหาอีกต่อไป ระบบประเภทจะยังคง "ถูกต้อง" (เสียง ฯลฯ ) ต้นไม้ที่ไม่มีที่สิ้นสุดที่เกิดขึ้นเมื่อบางครั้งเรียกว่า "ต้นไม้ที่มีเหตุผล" ระบบประเภทอ่อนแอลงนั่นคือตรวจสอบข้อผิดพลาดโปรแกรมเมอร์ไม่ได้

ดูการบรรยายของฉันในแลมบ์ดาแคลคูลัส (เริ่มที่สไลด์ 27) สำหรับข้อมูลเพิ่มเติมเกี่ยวกับผู้ให้บริการ fixpoint พร้อมตัวอย่างใน OCaml

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