Biểu diễn các biến bị ràng buộc với một hàm từ sử dụng đến chất kết dính


11

Vấn đề biểu diễn các biến bị ràng buộc theo cú pháp, và đặc biệt là thay thế tránh bắt, được biết đến và có một số giải pháp: biến được đặt tên với tương đương alpha, chỉ số de Bruijn, không tên cục bộ, bộ danh nghĩa, v.v.

Nhưng dường như có một cách tiếp cận khá rõ ràng khác, mà tôi chưa thấy được sử dụng ở bất cứ đâu. Cụ thể, trong cú pháp cơ bản, chúng ta chỉ có một thuật ngữ "biến", bằng văn bản nói , và sau đó riêng biệt chúng ta đưa ra một hàm ánh xạ mỗi biến thành một chất kết dính trong phạm vi của nó. Vì vậy, một -term nhưλλ

λx.(λy.xy)

sẽ được viết và chức năng sẽ ánh xạ đầu tiên sang thứ nhất và thứ hai đến thứ hai . Vì vậy, nó giống như các chỉ số de Bruijn, chỉ thay vì phải "đếm s" khi bạn rút lui khỏi thuật ngữ để tìm ra chất kết dính tương ứng, bạn chỉ cần đánh giá một hàm. (Nếu biểu diễn điều này như một cấu trúc dữ liệu trong một triển khai, tôi sẽ nghĩ đến việc trang bị cho mỗi đối tượng thuật ngữ biến với một con trỏ / tham chiếu đơn giản đến đối tượng kết nối tương ứng.)λ λ λλ.(λ.)λλλ

Rõ ràng điều này là không hợp lý để viết cú pháp trên một trang để con người đọc, nhưng sau đó không phải là chỉ số de Bruijn. Dường như với tôi rằng nó có ý nghĩa hoàn hảo về mặt toán học, và đặc biệt nó làm cho việc thay thế tránh bị bắt rất dễ dàng: chỉ cần bỏ qua thuật ngữ bạn đang thay thế và thực hiện liên kết các hàm ràng buộc. Đúng là nó không có khái niệm "biến tự do", nhưng sau đó (một lần nữa) cũng không có chỉ số de Bruijn thực sự; trong cả hai trường hợp, một thuật ngữ chứa các biến miễn phí được biểu thị một thuật ngữ với một danh sách các ràng buộc "bối cảnh" ở phía trước.

Tôi có thiếu một cái gì đó và có một số lý do đại diện này không hoạt động? Có vấn đề nào làm cho nó tồi tệ hơn nhiều so với những vấn đề khác mà nó không đáng để xem xét? (Vấn đề duy nhất tôi có thể nghĩ đến bây giờ là tập hợp các thuật ngữ (cùng với các chức năng ràng buộc của chúng) không được xác định theo quy nạp, nhưng điều đó dường như không thể vượt qua.) Hoặc có thực sự có nơi nào được sử dụng không?


2
Tôi không biết về nhược điểm. Có lẽ chính thức hóa (ví dụ trong một trợ lý bằng chứng) nặng hơn? Tôi không chắc chắn ... Điều tôi biết là không có gì sai về mặt kỹ thuật: cách nhìn này về thuật ngữ lambda là cách được đề xuất bởi đại diện của họ như là lưới chứng minh, vì vậy những người nhận biết bằng chứng (như tôi) hoàn toàn sử dụng nó mọi lúc Nhưng những người có bằng chứng về mạng rất hiếm :-) Vì vậy, có lẽ đó thực sự là vấn đề của truyền thống. PS: Tôi đã thêm một vài thẻ liên quan lỏng lẻo để làm cho câu hỏi rõ ràng hơn (hy vọng).
Damiano Mazza

Không phải cách tiếp cận này tương đương với cú pháp trừu tượng bậc cao (nghĩa là biểu thị các chất kết dính như các hàm trong ngôn ngữ máy chủ)? Theo một nghĩa nào đó, sử dụng một hàm như một chất kết dính sẽ thiết lập các con trỏ tới các chất kết dính ngầm, trong biểu diễn các bao đóng.
Rodolphe Lepigre

2
@RodolpheLepigre Tôi không nghĩ vậy. Cụ thể, sự hiểu biết của tôi là HOAS chỉ đúng khi siêu máy tính khá yếu, trong khi cách tiếp cận này là chính xác trong một siêu máy tính tùy ý.
Mike Shulman

3
Phải, vì vậy mỗi chất kết dính sử dụng một tên biến duy nhất (trong cây) (con trỏ tới nó là một tên tự động). Đây là quy ước Barendregt. Nhưng khi bạn thay thế, bạn phải xây dựng lại (bằng C) thứ bạn đang thay thế để tiếp tục có tên duy nhất. Mặt khác (nói chung) bạn đang sử dụng cùng một con trỏ cho nhiều cây con và có thể có được sự thay đổi. Việc xây dựng lại là đổi tên alpha. Có lẽ một cái gì đó tương tự xảy ra tùy thuộc vào chi tiết cụ thể của mã hóa cây của bạn dưới dạng tập hợp?
Dan Doel

3
@DanDoel Ah, thú vị. Tôi nghĩ rằng rõ ràng là không cần đề cập đến việc bạn sẽ bỏ một bản sao riêng của thuật ngữ được thay thế mỗi lần xuất hiện của biến mà nó được thay thế; nếu không bạn sẽ không có cây cú pháp nữa! Tôi đã không nghĩ rằng việc sao chép này là đổi tên alpha, nhưng bây giờ bạn chỉ ra rằng tôi có thể thấy nó.
Mike Shulman

Câu trả lời:


11

Câu trả lời của Andrej và Łukasz tạo ra những điểm tốt, nhưng tôi muốn thêm ý kiến ​​bổ sung.

Để lặp lại những gì Damiano nói, cách thể hiện ràng buộc này bằng cách sử dụng con trỏ là cách được đề xuất bởi các lưới bằng chứng, nhưng nơi đầu tiên tôi thấy nó cho các thuật ngữ lambda là trong một bài luận cũ của Knuth:

  • Donald Knuth (1970). Ví dụ về ngữ nghĩa chính thức. Trong Hội thảo chuyên đề về ngữ nghĩa của ngôn ngữ thuật toán , E. Engeler (chủ biên), Ghi chú bài giảng trong Toán học 188, Springer.

Trên trang 234, ông đã vẽ sơ đồ sau (mà ông gọi là "cấu trúc thông tin") đại diện cho thuật ngữ :(λy.λz.yz)x

Sơ đồ của Knuth cho $ (\ lambda y. \ Lambda z.yz) x $

Kiểu biểu diễn đồ họa này của các thuật ngữ lambda cũng được nghiên cứu độc lập (và sâu sắc hơn) trong hai luận án vào đầu những năm 1970, cả Christopher Wadsworth (1971, Semantics và thực dụng của Lambda-Compus ) và bởi Richard Statman (1974, Complex Complexity Bằng chứng ). Ngày nay, các sơ đồ như vậy thường được gọi là "biểu đồ" (xem ví dụ trong bài viết này ).

Quan sát rằng thuật ngữ trong sơ đồ của Knuth là tuyến tính , theo nghĩa là mọi biến tự do hoặc ràng buộc xảy ra chính xác một lần - như những người khác đã đề cập, có những vấn đề và lựa chọn không tầm thường được đưa ra khi cố gắng mở rộng loại biểu diễn này thành không điều khoản -linear.

Mặt khác, đối với các thuật ngữ tuyến tính, tôi nghĩ rằng nó thật tuyệt! Tuyến tính loại trừ nhu cầu sao chép và do đó, bạn nhận được cả tương đương và thay thế "miễn phí". Đây là những ưu điểm giống như HOAS và tôi thực sự đồng ý với Rodolphe Lepigre rằng có một mối liên hệ (nếu không chính xác là tương đương) giữa hai hình thức biểu diễn: có một ý nghĩa trong đó các cấu trúc đồ họa này có thể được hiểu một cách tự nhiên là sơ đồ chuỗi , đại diện cho sự biến đổi nội sinh của một vật thể phản xạ trong một thể loại khép kín nhỏ gọn (tôi đã giải thích ngắn gọn về điều đó ở đây ).α


10

Tôi không chắc làm thế nào chức năng biến-thành-chất kết dính của bạn sẽ được trình bày và cho mục đích gì bạn muốn sử dụng nó. Nếu bạn đang sử dụng các con trỏ ngược thì như Andrej lưu ý rằng độ phức tạp tính toán của sự thay thế không tốt hơn so với đổi tên alpha cổ điển.

Từ nhận xét của bạn về câu trả lời của Andrej, tôi suy luận rằng ở một mức độ nào đó bạn quan tâm đến việc chia sẻ. Tôi có thể cung cấp một số đầu vào ở đây.

Trong một phép tính lambda đánh máy điển hình, suy yếu và co lại, trái với các quy tắc khác, không có cú pháp.

Γ , x 1 : A , x 2 : A t : T

Γt:TΓ,x:At:TW
Γ,x1:A,x2:At:TΓ,x:At:TC

Hãy thêm một số cú pháp:

Γt:TΓ,x:AWx(t):TW
Γ,x1:A,x2:At:TΓ,x:ACxx1,x2(t):TC

a b , cCab,c() là 'sử dụng hết' biến và các biến ràng buộc . Tôi đã học được ý tưởng đó từ một trong những " Thực hiện giảm tương tác thuần" của Ian Mackie .ab,c

Với cú pháp đó, mỗi biến được sử dụng chính xác hai lần, một lần khi nó bị ràng buộc và một lần khi nó được sử dụng. Điều này cho phép chúng ta tạo khoảng cách với một cú pháp cụ thể và xem thuật ngữ này dưới dạng biểu đồ trong đó các biến và thuật ngữ là các cạnh.

Từ độ phức tạp thuật toán, giờ đây chúng ta có thể sử dụng các con trỏ không phải từ một biến thành một chất kết dính, mà từ chất kết dính đến biến số và có sự thay thế trong một thời gian không đổi.

Hơn nữa, cải cách này cho phép chúng tôi theo dõi việc xóa, sao chép và chia sẻ với độ trung thực cao hơn. Người ta có thể viết các quy tắc sao chép tăng dần (hoặc xóa) một thuật ngữ trong khi chia sẻ các tập con. Có nhiều cách để làm điều đó. Trong một số cài đặt hạn chế , chiến thắng là khá đáng ngạc nhiên .

Điều này đang tiến gần đến các chủ đề của mạng tương tác, tổ hợp tương tác, thay thế rõ ràng, logic tuyến tính, đánh giá tối ưu của Lamping, chia sẻ biểu đồ, logic nhẹ và khác.

Tất cả những chủ đề này rất thú vị đối với tôi và tôi sẵn sàng cung cấp các tài liệu tham khảo cụ thể hơn nhưng tôi không chắc liệu những điều này có hữu ích với bạn không và sở thích của bạn là gì.


6

Cấu trúc dữ liệu của bạn hoạt động nhưng nó sẽ không hiệu quả hơn các cách tiếp cận khác vì bạn cần sao chép mọi đối số trên mỗi lần giảm beta và bạn phải tạo ra nhiều bản sao khi có sự xuất hiện của biến bị ràng buộc. Bằng cách này, bạn tiếp tục phá hủy việc chia sẻ bộ nhớ giữa các subterms. Kết hợp với thực tế là bạn đang đề xuất một giải pháp không thuần túy liên quan đến các thao tác con trỏ và do đó rất dễ bị lỗi, có lẽ nó không đáng để gặp rắc rối.

Nhưng tôi rất vui khi thấy một thử nghiệm! Bạn có thể lambdathực hiện và triển khai nó với cấu trúc dữ liệu của mình (OCaml có con trỏ, chúng được gọi là tài liệu tham khảo ). Nhiều hay ít, bạn chỉ cần thay thế syntax.mlnorm.mlvới các phiên bản của bạn. Đó là ít hơn 150 dòng mã.


Cảm ơn! Tôi thừa nhận tôi đã không thực sự suy nghĩ rất nhiều về việc triển khai mà chủ yếu là về việc có thể làm bằng chứng toán học mà không bận tâm đến việc ghi sổ de Bruijn hoặc đổi tên alpha. Nhưng có bất kỳ cơ hội nào mà việc triển khai có thể giữ lại một số chia sẻ bộ nhớ bằng cách không tạo các bản sao "cho đến khi cần thiết", tức là cho đến khi các bản sao sẽ phân kỳ khỏi nhau?
Mike Shulman

Chắc chắn, bạn có thể thực hiện các thao tác sao chép, các hệ điều hành mà mọi người đã thực hiện các thủ thuật này trong một thời gian dài. Người ta sẽ phải cung cấp một số bằng chứng cho thấy nó sẽ hoạt động tốt hơn các giải pháp đã được thiết lập. Rất nhiều sẽ phụ thuộc vào mô hình sử dụng. Chẳng hạn, hầu hết các đối số cho -redeces đều bị trùng lặp, hoặc chúng chủ yếu được sử dụng theo kiểu tuyến tính? Trong một redex điển hình , cái nào thường lớn hơn, hoặc ? Nhân tiện, các trạm biến áp rõ ràng là một cách để làm mọi thứ một cách lười biếng là tốt. βe 1 e 2(λx.e1)e2e1e2
Andrej Bauer

2
Về các bằng chứng toán học, giờ đây tôi đã trải qua quá trình chính thức hóa cú pháp lý thuyết kiểu, kinh nghiệm của tôi là những lợi thế có được khi chúng ta khái quát hóa thiết lập và làm cho nó trừu tượng hơn, chứ không phải khi chúng ta làm cho nó cụ thể hơn. Chẳng hạn, chúng ta có thể tham số cú pháp với "bất kỳ cách xử lý ràng buộc nào tốt". Khi chúng ta làm như vậy, sẽ khó phạm sai lầm hơn. Tôi cũng đã chính thức loại lý thuyết với các chỉ số de Bruijn. Nó không quá khủng khiếp, đặc biệt là nếu bạn có các loại phụ thuộc xung quanh khiến bạn không thể làm những việc vô nghĩa.
Andrej Bauer

2
Để thêm vào, tôi đã thực hiện một triển khai sử dụng cơ bản kỹ thuật này (nhưng với các số nguyên và bản đồ duy nhất, không phải con trỏ) và tôi thực sự không khuyến nghị sử dụng nó. Chúng tôi chắc chắn đã có rất nhiều lỗi trong đó chúng tôi đã bỏ lỡ việc nhân bản mọi thứ một cách chính xác (một phần không nhỏ do cố gắng tránh nó khi có thể). Nhưng tôi nghĩ rằng có một bài báo của một số người GHC nơi họ ủng hộ nó (họ đã sử dụng hàm băm để tạo ra các tên duy nhất, tôi tin vậy). Nó có thể phụ thuộc chính xác những gì bạn đang làm. Trong trường hợp của tôi, đó là loại suy luận / kiểm tra, và nó có vẻ khá kém phù hợp ở đó.
Dan Doel

@MikeShulman Đối với các thuật toán có độ phức tạp (Tiểu học) hợp lý (với mức độ sao chép và xóa lớn), cái gọi là 'phần trừu tượng' của việc giảm tối ưu của Lamping không tạo ra các bản sao cho đến khi cần thiết. Phần trừu tượng cũng là phần không gây tranh cãi trái ngược với thuật toán đầy đủ đòi hỏi một số chú thích có thể chi phối tính toán.
Łukasz Lew

5

Các câu trả lời khác chủ yếu là thảo luận về các vấn đề thực hiện. Vì bạn đề cập đến động lực chính của bạn là làm bằng chứng toán học mà không cần quá nhiều sổ sách, đây là vấn đề chính tôi thấy với điều đó.

Khi bạn nói ra một chức năng mà ánh xạ mỗi biến thành một chất kết dính trong phạm vi của nó nằm trên phạm vi của nó: loại đầu ra của hàm này chắc chắn là một chút tinh vi hơn so với điều đó làm cho âm thanh! Cụ thể, hàm phải lấy các giá trị trong một cái gì đó giống như các ràng buộc của thuật ngữ được xem xét bởi - - một số tập hợp khác nhau tùy theo thuật ngữ (và rõ ràng không phải là tập hợp con của tập hợp môi trường lớn hơn theo bất kỳ cách hữu ích nào). Vì vậy, thay vào đó, bạn không thể chỉ lấy các liên kết của các hàm ràng buộc. Bạn cũng phải giới thiệu lại các giá trị của chúng, theo một số bản đồ từ các chất kết dính trong các điều khoản ban đầu đến các chất kết dính trong kết quả của sự thay thế.

Những reindexings này chắc chắn phải là thói quen của người Hồi giáo, theo nghĩa là chúng có thể được quét một cách hợp lý dưới tấm thảm, hoặc được đóng gói độc đáo theo một số loại functoriality hoặc naturality. Nhưng điều tương tự cũng đúng với sổ sách kế toán liên quan đến việc làm việc với các biến được đặt tên. Vì vậy, về tổng thể, có vẻ như tôi sẽ có ít nhất là nhiều sổ sách liên quan đến phương pháp này cũng như với các phương pháp tiêu chuẩn hơn.

Mặc dù vậy, đây là một cách tiếp cận rất hấp dẫn về mặt khái niệm và tôi rất muốn thấy nó được thực hiện một cách cẩn thận - tôi cũng có thể tưởng tượng nó có thể đưa ra một ánh sáng khác về một số khía cạnh của cú pháp so với các cách tiếp cận tiêu chuẩn.


theo dõi phạm vi của từng biến thực sự đòi hỏi phải có sổ sách kế toán, nhưng đừng đi đến kết luận rằng người ta luôn cần phải hạn chế cú pháp có phạm vi tốt! Các hoạt động như thay thế và giảm beta có thể được xác định ngay cả trên các thuật ngữ không chính xác và nghi ngờ của tôi là nếu muốn chính thức hóa phương pháp này (một lần nữa, thực sự là cách tiếp cận của lưới chứng minh / "-đồ thị") trong một trợ lý bằng chứng, trước tiên người ta sẽ thực hiện các hoạt động chung hơn, và sau đó chứng minh rằng họ bảo vệ tài sản của phạm vi tốt.
Noam Zeilberger

(Đồng ý rằng nó đáng để thử ... mặc dù tôi sẽ không ngạc nhiên nếu ai đó đã có trong bối cảnh chính thức hóa các lưới /-đồ thị.)
Noam Zeilberger


5

Đây là nỗ lực của tôi trong việc mã hóa -calculus bằng cách sử dụng phương pháp của bạn (bằng OCaml, với một số giải thích trong các nhận xét). Thực tế có thể định nghĩa các thuật ngữ là giá trị vòng tròn, có nghĩa là đại diện này có cơ hội tốt để hoạt động tốt trong Coq. Lưu ý rằng nó sẽ yêu cầu một loại cưỡng chế trong biểu diễn các bao đóng (để giải thích cho việc tôi sử dụng bên dưới).λLazy.t

Nhìn chung, tôi nghĩ rằng đó là một đại diện tuyệt vời, nhưng nó liên quan đến một số sổ sách kế toán với con trỏ, để tránh phá vỡ các liên kết ràng buộc. Tôi có thể thay đổi mã để sử dụng các trường có thể thay đổi mà tôi đoán, nhưng mã hóa trong Coq sau đó sẽ ít trực tiếp hơn. Tôi vẫn tin rằng điều này rất giống với HOAS, mặc dù cấu trúc con trỏ được làm rõ ràng. Tuy nhiên, sự hiện diện của Lazy.thàm ý rằng một số mã có thể được đánh giá không đúng lúc. Đây không phải là trường hợp trong mã của tôi vì chỉ thay thế một biến bằng một biến có thể xảy ra tại forcethời điểm (và không đánh giá chẳng hạn).

(* Representation of a term of the λ-calculus. *)
type term =
  | FVar of string      (* Free variable  *)
  | BVar of bvar        (* Bound variable *)
  | Appl of term * term (* Application    *)
  | Abst of abst        (* Abstraction    *)

(* A bound variable is a pointer to the corresponding binder. *)
and bvar = abst

(* A binder is represented as its body in which the bound variable points to
   the binder itself. Note that we need to use a thunk to be able to work
   underneath a binder (for substitution, evaluation, ...). A name can be
   given for easy printing, but no renaming is done. Only “visual capture”
   can happen since pointers are established the right way, even if names
   can clash. *)
and abst = { body : term Lazy.t ; name : string }

(* Terms can be built with recursive values for abstractions. *)

(* Krivine's notation is used for application (function in parentheses). *)

let id    : term = (* λx.x        *)
  Abst(let rec id = {body = lazy (BVar(id)); name = "x"} in id)

let idid  : term = (* (λx.x) λx.x *)
  Appl(id, id)

let delta : term = (* λx.(x) x *)
  Abst(let rec d = {body = lazy (Appl(BVar(d), BVar(d))); name = "x" } in d)

let weird : term = (* (λx.x) λy.(λx.(x) x) (C) y *)
  Appl(id, Abst(let rec x = {body = lazy (Appl(delta, Appl(FVar("C"),
    BVar(x)))); name = "y"} in x))

let omega : term = (* (λx.(x) x) λx.(x) x *)
  Appl(delta, delta)

(* Printing function is immediate. *)
let rec print : out_channel -> term -> unit = fun oc t ->
  match t with
  | FVar(x)   -> output_string oc x
  | BVar(x)   -> output_string oc x.name
  | Appl(t,u) -> Printf.fprintf oc "(%a) %a" print t print u
  | Abst(f)   -> Printf.fprintf oc "λ%s.%a" f.name print (Lazy.force f.body)

(* Substitution of variable [x] by [v] in the term [t]. Occurences of [x] in
   [t] are identified using physical equality ([BVar] case). The subtle case
   is [Abst], because we need to reestablish the physical link between the
   binder and the variable it binds. *)
let rec subst_var : bvar -> term -> term -> term = fun x t v ->
  match t with
  | FVar(_)   -> t
  | BVar(y)   -> if y == x then v else t
  | Appl(t,u) -> Appl(subst_var x t v, subst_var x u v)
  | Abst(f)   ->
      (* First compute the new body. *)
      let fv = subst_var x (Lazy.force f.body) v in
      (* Reestablish the physical link, using [subst_var] itself again. This
         requires a second traversal of the term. We could probably do both
         at once, but who cares the complexity is linear in [t] anyway. *)
      Abst(let rec g = {f with body = lazy (subst_var f fv (BVar(g)))} in g)

(* Actual substitution function. *)
let subst : abst -> term -> term = fun f v ->
  subst_var f (Lazy.force f.body) v

(* Normalization function (all the way, even under binders). *)
let rec eval : term -> term = fun t ->
  match t with
  | Appl(t,u) ->
      begin
        let v = eval u in
        match eval t with
        | Abst(f) -> eval (subst f v)
        | t       -> Appl(t,v)
      end
  | Abst(f)   ->
      (* Actual computation in the body. *)
      let fv = eval (Lazy.force f.body) in
      (* Here, the physical link is reestablished, but it is important to note
         that the computation of evaluation is done above. So the part below
         only takes a linear time in the size of the normal form of the body
         of the abstraction. *)
      Abst(let rec g = {f with body = lazy (subst_var f fv (BVar(g)))} in g)
  | _         ->
      t

let _ = Printf.printf "id         = %a\n%!" print id
let _ = Printf.printf "eval id    = %a\n%!" print (eval id)

let _ = Printf.printf "idid       = %a\n%!" print idid
let _ = Printf.printf "eval idid  = %a\n%!" print (eval idid)

let _ = Printf.printf "delta      = %a\n%!" print delta
let _ = Printf.printf "eval delta = %a\n%!" print (eval delta)

let _ = Printf.printf "omega      = %a\n%!" print omega
(* The following obviously loops. *)
(*let _ = Printf.printf "eval omega = %a\n%!" print (eval omega)*)

let _ = Printf.printf "weird      = %a\n%!" print weird
let _ = Printf.printf "eval weird = %a\n%!" print (eval weird)

(* Output produced:
id         = λx.x
eval id    = λx.x
idid       = (λx.x) λx.x
eval idid  = λx.x
delta      = λx.(x) x
eval delta = λx.(x) x
omega      = (λx.(x) x) λx.(x) x
weird      = (λx.x) λy.(λx.(x) x) (C) y
eval weird = λy.((C) y) (C) y
*)
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.