Xác định danh sách chỉ sử dụng hệ thống loại Hindley-Milner


10

Tôi đang làm việc trên một trình biên dịch tính toán lambda nhỏ có hệ thống suy luận kiểu Hindley-Milner đang hoạt động và hiện cũng hỗ trợ đệ quy (không phải trong mã được liên kết), mà tôi hiểu là đủ để làm cho nó hoàn chỉnh .

Vấn đề bây giờ là tôi không biết làm thế nào để làm cho nó hỗ trợ danh sách, hoặc liệu nó đã hỗ trợ chúng chưa và tôi chỉ cần tìm cách mã hóa chúng. Tôi muốn có thể xác định chúng mà không cần phải thêm quy tắc mới vào hệ thống loại.

Cách dễ nhất tôi có thể nghĩ về một danh sách xlà một cái gì đó là null(hoặc danh sách trống) hoặc một cặp chứa cả xdanh sách và danh sách x. Nhưng để làm điều này, tôi cần có khả năng xác định các cặp và hoặc, mà tôi tin là các loại sản phẩm và tổng.

Có vẻ như tôi có thể định nghĩa các cặp theo cách này:

pair = λabf.fab
first = λp.p(λab.a)
second = λp.p(λab.b)

pairsẽ có loại a -> (b -> ((a -> (b -> x)) -> x)), sau khi vượt qua, giả sử, intvà một string, nó sẽ mang lại một cái gì đó với loại (int -> (string -> x)) -> x, đó sẽ là đại diện của một cặp intstring. Điều làm phiền tôi ở đây là nếu điều đó đại diện cho một cặp, tại sao điều đó không tương đương về mặt logic, cũng không bao hàm mệnh đề int and string? Tuy nhiên, tương đương với (((int and string) -> x) -> x), như thể tôi chỉ có thể có các loại sản phẩm làm tham số cho các chức năng. Câu trả lời nàydường như để giải quyết vấn đề này, nhưng tôi không biết những biểu tượng anh ấy sử dụng có ý nghĩa gì. Ngoài ra, nếu điều này không thực sự mã hóa một loại sản phẩm, tôi có thể làm gì với các loại sản phẩm mà tôi không thể làm với định nghĩa về các cặp ở trên không (xem xét tôi cũng có thể định nghĩa n-tuples theo cách tương tự)? Nếu không, điều này có mâu thuẫn với thực tế là bạn không thể diễn đạt (AFAIK) chỉ sử dụng hàm ý không?

Ngoài ra, làm thế nào về các loại tổng? Tôi có thể bằng cách nào đó mã hóa nó bằng cách chỉ sử dụng loại chức năng? Nếu vậy, điều này sẽ đủ để xác định danh sách? Hoặc nếu không, có cách nào khác để xác định danh sách mà không phải mở rộng hệ thống loại của tôi không? Và nếu không, tôi cần phải thay đổi những gì nếu tôi muốn giữ nó đơn giản nhất có thể?

Xin lưu ý rằng tôi là một lập trình viên máy tính nhưng không phải là nhà khoa học máy tính cũng không phải là nhà toán học và khá tệ trong việc đọc ký hiệu toán học.

Chỉnh sửa: Tôi không chắc tên kỹ thuật của những gì tôi đã triển khai cho đến nay, nhưng tất cả những gì tôi có về cơ bản là mã tôi đã liên kết ở trên, đây là thuật toán tạo ràng buộc sử dụng quy tắc cho các ứng dụng, trừu tượng và biến được thực hiện từ thuật toán Hinley-Milner và sau đó là thuật toán hợp nhất lấy loại chính. Chẳng hạn, biểu thức \a.asẽ mang lại kiểu a -> avà biểu thức \a.(a a)sẽ đưa ra lỗi kiểm tra xảy ra. Trên hết, không có chính xác một letquy tắc mà là một hàm dường như có cùng hiệu ứng cho phép bạn xác định các hàm toàn cầu đệ quy như mã giả này:

GetTypeOfGlobalFunction(term, globalScope, nameOfFunction)
{
    // Here 'globalScope' contains a list of name-value pair where every value is of class 'ClosedType', 
    // meaning their type will be cloned before unified in the unification algorithm so that they can be used polymorphically 
    tempType = new TypeVariable() // Assign a dummy type to `tempType`, say, type 'x'.
    // The next line creates an scope with everything in 'globalScope' plus the 'nameOfFunction = tempType' name-value pair
    tempScope = new Scope(globalScope, nameOfFunction, tempType) 
    type = TypeOfTerm(term, tempScope) // Calculate the type of the term 
    Unify(tempType, type)
    return type
    // After returning, the code outside will create a 'ClosedType' using the returned type and add it to the global scope.
}

Mã về cơ bản lấy loại thuật ngữ như bình thường, nhưng trước khi thống nhất, nó thêm tên của hàm được định nghĩa với loại giả vào phạm vi loại để có thể sử dụng đệ quy từ bên trong chính nó.

Chỉnh sửa 2: Tôi mới nhận ra rằng tôi cũng cần các kiểu đệ quy mà tôi không có để xác định danh sách như tôi muốn.


Bạn có thể nói rõ hơn một chút về chính xác những gì bạn đã thực hiện không? Bạn đã thực hiện phép tính lambda được gõ đơn giản (với các định nghĩa đệ quy) và đưa ra cho nó đa hình tham số theo kiểu Hindley-Milner chưa? Hoặc bạn đã thực hiện phép tính lambda đa hình bậc hai?
Andrej Bauer

Có lẽ tôi có thể hỏi một cách dễ dàng hơn: nếu tôi dùng OCaml hoặc SML và hạn chế nó theo các thuật ngữ lambda thuần túy và các định nghĩa đệ quy, đó có phải là điều bạn đang nói đến không?
Andrej Bauer

@AndrejBauer: Tôi đã chỉnh sửa câu hỏi. Tôi không chắc chắn về OCaml và SML, nhưng tôi khá chắc chắn nếu bạn sử dụng Haskell và hạn chế nó theo các điều khoản lambda và đệ quy cấp cao nhất cho phép (chẳng hạn let func = \x -> (func x)) bạn có được những gì tôi có.
Juan

1
Để có thể cải thiện câu hỏi của bạn, hãy kiểm tra bài meta này .
Juho

Câu trả lời:


13

Cặp

Mã hóa này là mã hóa Giáo hội của các cặp. Các kỹ thuật tương tự có thể mã hóa booleans, số nguyên, danh sách và các cấu trúc dữ liệu khác.

x:a; y:bpair x y(a -> b -> t) -> t¬

(abt)t¬(¬a¬bt)t(ab¬t)t(ab)t
ab tpairt

pairlà một hàm tạo cho kiểu cặp firstsecondlà hàm hủy. (Đây là những từ giống nhau được sử dụng trong lập trình hướng đối tượng; ở đây các từ có nghĩa liên quan đến việc giải thích logic các loại và thuật ngữ mà tôi sẽ không đi vào đây.) Theo trực giác, các hàm hủy cho phép bạn truy cập vào những gì trong đối tượng và các hàm tạo sẽ mở đường cho hàm hủy bằng cách lấy làm đối số một hàm mà chúng áp dụng cho các phần của đối tượng. Nguyên tắc này có thể được áp dụng cho các loại khác.

Tổng

Mã hóa Giáo hội của một liên minh bị phân biệt đối xử về cơ bản là kép đối với mã hóa Giáo hội của một cặp. Khi một cặp có hai phần phải được đặt cùng nhau và bạn có thể chọn trích xuất cái này hoặc cái kia, bạn có thể chọn xây dựng liên minh theo một trong hai cách và khi bạn sử dụng nó, bạn cần cho phép cả hai cách. Do đó, có hai hàm tạo và có một hàm hủy duy nhất có hai đối số.

let case w = λf. λg. w f g           case : ((a->t) -> (b->t) -> t) -> (a->t) -> (b->t) -> t
  (* or simply let case w = w *)
let left x = λf. λg. f x             left : a -> ((a->t) -> (b->t) -> t)
let right y = λf. λg. g x            right : b -> ((a->t) -> (b->t) -> t)

Hãy để tôi viết tắt các loại (a->t) -> (b->t) -> tnhư SUM(a,b)(t). Sau đó, các loại của hàm hủy và hàm tạo là:

case : SUM(a,b)(t) -> (a->t) -> (b->t) -> t
left : a -> SUM(a,b)(t)
right : b -> SUM(a,b)(t)

Như vậy

case (left x) f g → f x
case (rightt y) f g → g y

Danh sách

Đối với một danh sách, áp dụng một lần nữa cùng một nguyên tắc. Một danh sách có các phần tử có loại acó thể được xây dựng theo hai cách: nó có thể là một danh sách trống hoặc nó có thể là một phần tử (phần đầu) cộng với một danh sách (phần đuôi). So với các cặp, có một chút thay đổi khi nói đến các hàm hủy: bạn không thể có hai hàm hủy riêng biệt headtailvì chúng chỉ hoạt động trên các danh sách không trống. Bạn cần một hàm hủy duy nhất, với hai đối số, một trong số đó là hàm 0 đối số (nghĩa là giá trị) cho trường hợp không và hàm kia là đối số 2 cho trường hợp khuyết điểm. Các chức năng như is_empty, headtailcó thể được bắt nguồn từ đó. Giống như trong trường hợp tổng, danh sách là hàm hủy trực tiếp của chính nó.

let nil = λn. λc. n
let cons h t = λn. λc. c h t
let is_empty l = l true (λh. λt. false) 
let head l default = l default (λh. λt. h)
let tail l default = l default (λh. λt. t)

consconsconsTT1,,Tn

Khi bạn phỏng đoán, nếu bạn muốn xác định một loại chỉ chứa các danh sách đồng nhất, bạn cần các loại đệ quy. Tại sao? Hãy nhìn vào loại danh sách. Một danh sách được mã hóa dưới dạng một hàm có hai đối số: giá trị trả về trên danh sách trống và hàm để tính giá trị trả về trên một ô khuyết điểm. Hãy alà kiểu phần tử, blà kiểu của danh sách và clà kiểu được trả về bởi hàm hủy. Loại danh sách là

a -> (a -> b -> c) -> c

Làm cho danh sách đồng nhất là nói rằng nếu đó là một ô khuyết điểm, đuôi phải có cùng loại với toàn bộ, nghĩa là nó thêm ràng buộc

a -> (a -> b -> c) -> c = b

Hệ thống loại Hindley-Milner có thể được mở rộng với các kiểu đệ quy như vậy và trên thực tế, các ngôn ngữ lập trình thực tế làm được điều đó. Các ngôn ngữ lập trình thực tế có xu hướng không cho phép các phương trình trần trụi như vậy và yêu cầu một nhà xây dựng dữ liệu, nhưng đây không phải là một yêu cầu nội tại của lý thuyết cơ bản. Yêu cầu một hàm tạo dữ liệu đơn giản hóa suy luận kiểu và trong thực tế có xu hướng tránh việc chấp nhận các hàm thực sự có lỗi nhưng tình cờ có thể đánh máy được với một số ràng buộc ngoài ý muốn gây ra lỗi loại khó hiểu khi sử dụng hàm. Đây là lý do tại sao, ví dụ, OCaml chấp nhận các kiểu đệ quy không được bảo vệ chỉ với -rectypestùy chọn trình biên dịch không mặc định . Dưới đây là các định nghĩa ở trên theo cú pháp OCaml, cùng với định nghĩa kiểu cho các danh sách đồng nhất bằng cách sử dụng ký hiệu chocác kiểu đệ quy bí danh : type_expression as 'acó nghĩa là loại type_expressionđược thống nhất với biến 'a.

# let nil = fun n c -> n;;
val nil : 'a -> 'b -> 'a = <fun>
# let cons h t = fun n c -> c h t;;
val cons : 'a -> 'b -> 'c -> ('a -> 'b -> 'd) -> 'd = <fun>
# let is_empty l = l true (fun h t -> false);;
val is_empty : (bool -> ('a -> 'b -> bool) -> 'c) -> 'c = <fun>
# let head l default = l default (fun h t -> h);;
val head : ('a -> ('b -> 'c -> 'b) -> 'd) -> 'a -> 'd = <fun>
# let tail l default = l default (fun h t -> t);;
val tail : ('a -> ('b -> 'c -> 'c) -> 'd) -> 'a -> 'd = <fun>
# type ('a, 'b, 'c) ulist = 'c -> ('a -> 'b -> 'c) -> 'c;;
type ('a, 'b, 'c) ulist = 'c -> ('a -> 'b -> 'c) -> 'c
# is_empty (cons 1 nil);;
- : bool = false
# head (cons 1 nil) 0;;
- : int = 1
# head (tail (cons 1 (cons 2.0 nil)) nil) 0.;;
- : float = 2.

(* -rectypes is required for what follows *)
# type ('a, 'b, 'c) rlist = 'c -> ('a -> 'b -> 'c) -> 'c as 'b;;
type ('a, 'b, 'c) rlist = 'b constraint 'b = 'c -> ('a -> 'b -> 'c) -> 'c
# let rcons = (cons : 'a -> ('a, 'b, 'c) rlist -> ('a, 'b, 'c) rlist);;
val rcons :
  'a ->
  ('a, 'c -> ('a -> 'b -> 'c) -> 'c as 'b, 'c) rlist -> ('a, 'b, 'c) rlist =
  <fun>
# head (rcons 1 (rcons 2 nil)) 0;;
- : int = 1
# tail (rcons 1 (rcons 2 nil)) nil;;
- : 'a -> (int -> 'a -> 'a) -> 'a as 'a = <fun>
# rcons 1 (rcons 2.0 nil);;
Error: This expression has type
         (float, 'b -> (float -> 'a -> 'b) -> 'b as 'a, 'b) rlist = 'a
       but an expression was expected of type
         (int, 'b -> (int -> 'c -> 'b) -> 'b as 'c, 'b) rlist = 'c

Nếp gấp

Nhìn vào điều này một chút tổng quát hơn, chức năng đại diện cho cấu trúc dữ liệu là gì?

  • nn
  • (x,y)xy
  • ini(x)ix
  • [x1,,xn]

Nói chung, cấu trúc dữ liệu được biểu diễn dưới dạng hàm gấp của nó . Đây là một khái niệm chung cho các cấu trúc dữ liệu: hàm gập là hàm bậc cao hơn đi ngang qua cấu trúc dữ liệu. Có một ý nghĩa kỹ thuật trong đó nếp gấp là phổ quát : tất cả các giao dịch cấu trúc dữ liệu chung chung có thể được thể hiện dưới dạng nếp gấp. Cấu trúc dữ liệu có thể được biểu diễn dưới dạng hàm gấp của nó cho thấy điều này: tất cả những gì bạn cần biết về cấu trúc dữ liệu là làm thế nào để vượt qua nó, phần còn lại là một chi tiết triển khai.


Bạn đề cập đến mã hóa của Church Church Các số nguyên, cặp, tổng, nhưng đối với các danh sách bạn cung cấp mã hóa Scott . Tôi nghĩ rằng nó có thể hơi khó hiểu cho những người không quen thuộc với mã hóa các loại quy nạp.
Stéphane Gimenez

Vì vậy, về cơ bản, loại cặp của tôi không thực sự là một loại sản phẩm vì chức năng với loại này chỉ có thể trả về tvà bỏ qua đối số được cho là sẽ lấy ab(đó chính xác (a and b) or tlà những gì đang nói). Và dường như tôi cũng gặp rắc rối tương tự với các khoản tiền. Và ngoài ra, không có loại đệ quy, tôi sẽ không có danh sách đồng nhất. Vì vậy, trong một vài từ, bạn đang nói rằng tôi nên thêm các quy tắc loại tổng, sản phẩm và đệ quy để có được danh sách đồng nhất?
Juan

Ý của bạn là case (right y) f g → g yở cuối phần Sums của bạn ?
Juan

@ StéphaneGimenez Tôi đã không nhận ra. Tôi không quen làm việc với các bảng mã này trong một thế giới đánh máy. Bạn có thể đưa ra một tài liệu tham khảo cho mã hóa Church vs mã hóa Scott?
Gilles 'SO- ngừng trở nên xấu xa'

@JuanLuisSoldi Có lẽ bạn đã nghe nói rằng không có vấn đề gì không thể giải quyết được với một mức độ bổ sung không rõ ràng. Mã hóa nhà thờ mã hóa các cấu trúc dữ liệu dưới dạng các hàm bằng cách thêm một mức gọi hàm: cấu trúc dữ liệu trở thành hàm bậc hai mà bạn áp dụng cho hàm để hành động trên các phần. Nếu bạn muốn một loại danh sách đồng nhất, bạn sẽ phải đối phó với thực tế là loại đuôi giống như loại của toàn bộ danh sách. Tôi nghĩ rằng điều này phải liên quan đến một hình thức đệ quy loại.
Gilles 'SO- ngừng trở nên xấu xa'

2

Bạn có thể biểu diễn các loại tổng dưới dạng các loại sản phẩm với các thẻ và giá trị. Trong trường hợp này, chúng ta có thể gian lận một chút và sử dụng một thẻ để thể hiện null hoặc không, có thẻ thứ hai đại diện cho cặp đầu / đuôi.

Chúng tôi định nghĩa booleans theo cách thông thường:

true = λi.λe.i
false = λi.λe.e
if = λcond.λthen.λelse.(cond then else)

Một danh sách sau đó là một cặp với phần tử đầu tiên là boolean và phần tử thứ hai là cặp đầu / đuôi. Một số chức năng danh sách cơ bản:

isNull = λl.(first l)
null = pair false false     --The second element doesn't matter in this case
cons = λh.λt.(pair true (pair h t ))
head = λl.(fst (snd l))   --This is a partial function
tail = λl.(snd (snd l))   --This is a partial function  

map = λf.λl.(if (isNull l)
                 null 
                 (cons (f (head l)) (map f (tail l) ) ) 

Nhưng điều này sẽ không cho tôi danh sách đồng nhất, điều này có đúng không?
Juan
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.