Loại kiểm tra và đệ quy (Viết bộ kết hợp Y bằng Haskell / Ocaml)


21

Khi giải thích bộ kết hợp Y trong ngữ cảnh của Haskell, người ta thường lưu ý rằng việc triển khai thẳng sẽ không kiểm tra kiểu trong Haskell vì loại đệ quy của nó.

Ví dụ: từ 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))

Và thực tế, định nghĩa rõ ràng của Viking không gõ kiểm tra:

λ> 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)

Giới hạn tương tự tồn tại trong 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

Tuy nhiên, trong Ocaml, người ta có thể cho phép các loại đệ quy bằng cách chuyển qua công -rectypestắc:

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

Bằng cách sử dụng -rectypes, mọi thứ hoạt động:

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

Tò mò về hệ thống loại và suy luận kiểu, điều này đặt ra một số câu hỏi tôi vẫn không thể trả lời.

  • Đầu tiên, làm thế nào để trình kiểm tra loại đến với loại t2 = t2 -> t0 -> t1? Đã đưa ra loại đó, tôi đoán vấn đề là loại ( t2) đề cập đến chính nó ở phía bên phải?
  • Thứ hai, và có lẽ thú vị nhất, lý do nào khiến các hệ thống loại Haskell / Ocaml không cho phép điều này? Tôi đoán có một lý do chính đáng kể từ Ocaml cũng sẽ không cho phép nó theo mặc định ngay cả khi nó có thể đối phó với các loại đệ quy nếu có -rectypeschuyển đổi.

Nếu đây là những chủ đề thực sự lớn, tôi sẽ đánh giá cao con trỏ đến các tài liệu liên quan.

Câu trả lời:


16

Đầu tiên, lỗi GHC,

GHC đang cố gắng thống nhất một vài ràng buộc với x, trước tiên, chúng tôi sử dụng nó như một chức năng

x :: a -> b

Tiếp theo chúng ta sử dụng nó như một giá trị cho hàm đó

x :: a

Và cuối cùng chúng tôi thống nhất nó với biểu thức đối số ban đầu

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

Bây giờ x xtrở thành một nỗ lực để thống nhất t2 -> t1 -> t0, tuy nhiên, Chúng tôi không thể thống nhất điều này vì nó sẽ yêu cầu thống nhất t2, đối số đầu tiên của x, với x. Do đó thông báo lỗi của chúng tôi.

Tiếp theo, tại sao không loại đệ quy chung. Điểm đáng chú ý đầu tiên là sự khác biệt giữa các loại đệ quy Equi và iso,

  • Equi-đệ quy là những gì bạn mong đợi mu X . Typechính xác tương đương với việc mở rộng hoặc gấp nó tùy ý.
  • Các kiểu đệ quy cung cấp một cặp toán tử, foldunfoldgấp và mở ra các định nghĩa đệ quy của các kiểu.

Bây giờ các loại đệ quy âm thanh lý tưởng, nhưng rất khó để có được ngay trong các hệ thống loại phức tạp. Nó thực sự có thể làm cho việc kiểm tra loại không thể giải quyết được. Tôi không quen thuộc với từng chi tiết của hệ thống loại của OCaml nhưng các kiểu tương đương hoàn toàn trong Haskell có thể khiến trình đánh máy lặp đi lặp lại một cách tùy tiện để cố gắng thống nhất các loại, theo mặc định, Haskell đảm bảo rằng việc kiểm tra kiểu đó chấm dứt. Hơn nữa, trong Haskell, các từ đồng nghĩa loại là ngu ngốc, các loại đệ quy hữu ích nhất sẽ được định nghĩa như thế nào type T = T -> (), tuy nhiên được nội tuyến gần như ngay lập tức trong Haskell, nhưng bạn không thể nội tuyến một loại đệ quy, nó là vô hạn! Do đó, các loại đệ quy trong Haskell sẽ yêu cầu một cuộc đại tu lớn về cách xử lý các từ đồng nghĩa, có lẽ không đáng để nỗ lực đặt ngay cả như một phần mở rộng ngôn ngữ.

Các loại đệ quy hơi khó sử dụng, bạn ít nhiều phải nói rõ cho người kiểm tra loại cách gấp và mở các loại của bạn, làm cho chương trình của bạn trở nên phức tạp hơn để đọc và viết.

Tuy nhiên, điều này rất giống với những gì bạn làm với Muloại của bạn . Rolllà gấp, và unrollđược mở ra. Vì vậy, trên thực tế, chúng ta có các kiểu đệ quy iso. Tuy nhiên, các kiểu đệ quy chỉ quá phức tạp nên các hệ thống như OCaml và Haskell buộc bạn phải vượt qua các lần lặp lại thông qua các điểm cố định mức loại.

Bây giờ nếu điều này làm bạn quan tâm, tôi muốn giới thiệu Loại và Ngôn ngữ lập trình. Bản sao của tôi đang mở trong lòng tôi khi tôi viết điều này để đảm bảo rằng tôi đã có thuật ngữ đúng :)


Cụ thể, chương 21 cung cấp một trực giác tốt cho các loại cảm ứng, cưỡng chế và đệ quy
Daniel Gratzer

Cảm ơn bạn! Điều này thực sự hấp dẫn. Tôi hiện đang đọc TAPL và tôi rất vui khi biết điều này sẽ được đề cập sau trong cuốn sách.
beta

@beta Yep, TAPL và đó là người anh lớn, Chủ đề nâng cao về các loại và Ngôn ngữ lập trình, là những tài nguyên tuyệt vời.
Daniel Gratzer

2

Trong OCaml, bạn cần truyền -rectypesdưới dạng tham số cho trình biên dịch (hoặc nhập #rectypes;;vào toplevel). Nói một cách đơn giản, điều này sẽ tắt "kiểm tra xảy ra" trong quá trình thống nhất. Tình hình The type variable 'a occurs inside 'a -> 'bsẽ không còn là một vấn đề. Hệ thống loại vẫn sẽ là "chính xác" (âm thanh, v.v.), các cây vô hạn phát sinh như các loại đôi khi được gọi là "cây hợp lý". Hệ thống loại trở nên yếu hơn, tức là không thể phát hiện ra một số lỗi lập trình viên.

Xem bài giảng của tôi về lambda-compus (bắt đầu từ slide 27) để biết thêm thông tin về các toán tử fixpoint với các ví dụ trong OCaml.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.