Làm thế nào để tạo ra một ngôn ngữ đồng âm


16

Theo bài viết này , dòng mã Lisp sau đây in "Hello world" thành đầu ra tiêu chuẩn.

(format t "hello, world")

Lisp, một ngôn ngữ đồng âm , có thể coi mã là dữ liệu theo cách này:

Bây giờ hãy tưởng tượng rằng chúng ta đã viết macro sau:

(defmacro backwards (expr) (reverse expr))

ngược là tên của macro, lấy một biểu thức (được biểu diễn dưới dạng danh sách) và đảo ngược nó. Đây là "Xin chào, thế giới" một lần nữa, lần này sử dụng macro:

(backwards ("hello, world" t format))

Khi trình biên dịch Lisp nhìn thấy dòng mã đó, nó nhìn vào nguyên tử đầu tiên trong danh sách ( backwards) và thông báo rằng nó đặt tên cho một macro. Nó chuyển danh sách không được ("hello, world" t format)đánh giá sang macro, sắp xếp lại danh sách (format t "hello, world"). Danh sách kết quả thay thế biểu thức macro và đó là những gì sẽ được đánh giá tại thời gian chạy. Môi trường Lisp sẽ thấy rằng nguyên tử đầu tiên của nó ( format) là một hàm và đánh giá nó, chuyển nó sang phần còn lại của các đối số.

Trong Lisp, việc đạt được nhiệm vụ này rất dễ dàng (sửa tôi nếu tôi sai) vì mã được triển khai dưới dạng danh sách ( s-biểu thức ?).

Bây giờ hãy xem đoạn trích OCaml này (không phải là ngôn ngữ đồng âm):

let print () =
    let message = "Hello world" in
    print_endline message
;;

Hãy tưởng tượng bạn muốn thêm đồng âm vào OCaml, sử dụng cú pháp phức tạp hơn nhiều so với Lisp. Bạn làm điều đó như thế nào? Liệu ngôn ngữ phải có một cú pháp đặc biệt dễ dàng để đạt được đồng âm?

EDIT : từ chủ đề này, tôi đã tìm ra một cách khác để đạt được tính đồng âm khác với Lisp: cách thực hiện bằng ngôn ngữ io . Nó có thể trả lời một phần câu hỏi này.

Ở đây, hãy bắt đầu với một khối đơn giản:

Io> plus := block(a, b, a + b)
==> method(a, b, 
        a + b
    )
Io> plus call(2, 3)
==> 5

Được rồi, vì vậy khối hoạt động. Khối cộng thêm hai số.

Bây giờ chúng ta hãy làm một số nội tâm về người bạn nhỏ này.

Io> plus argumentNames
==> list("a", "b")
Io> plus code
==> block(a, b, a +(b))
Io> plus message name
==> a
Io> plus message next
==> +(b)
Io> plus message next name
==> +

Nóng lạnh khuôn lạnh. Bạn không chỉ có thể nhận được tên của các thông số khối. Và không chỉ bạn có thể có được một chuỗi mã nguồn hoàn chỉnh của khối. Bạn có thể lẻn vào mã và duyệt các tin nhắn bên trong. Và tuyệt vời nhất trong tất cả: nó cực kỳ dễ dàng và tự nhiên. Đúng như nhiệm vụ của Io. Gương của Ruby không thể thấy bất cứ thứ gì trong số đó.

Nhưng, whoa whoa, hey, đừng chạm vào mặt số đó.

Io> plus message next setName("-")
==> -(b)
Io> plus
==> method(a, b, 
        a - b
    )
Io> plus call(2, 3)
==> -1

1
Bạn có thể muốn xem Scala đã thực hiện các macro
Bergi

1
@Bergi Scala có một cách tiếp cận mới về macro: scala.meta .
Martin Berger

Tôi đã luôn luôn mặc dù đồng âm được đánh giá cao. Trong bất kỳ ngôn ngữ nào đủ mạnh, bạn luôn có thể xác định cấu trúc cây phản ánh cấu trúc của chính ngôn ngữ đó và các hàm tiện ích có thể được viết để dịch sang và từ ngôn ngữ nguồn (và / hoặc một dạng được biên dịch) theo yêu cầu. Đúng, nó dễ dàng hơn một chút trong các LISP, nhưng vì (a) phần lớn công việc lập trình không nên được lập trình siêu hình và (b) LISP đã hy sinh sự rõ ràng về ngôn ngữ để biến điều này thành có thể, tôi không nghĩ rằng sự đánh đổi là đáng giá.
Periata Breatta

@PeriataBreatta Bạn nói đúng, nhưng lợi thế chính của MP là MP cho phép trừu tượng hóa mà không bị phạt trong thời gian chạy . Do đó, MP giải quyết căng thẳng giữa trừu tượng và hiệu suất, mặc dù phải trả giá bằng việc tăng độ phức tạp của ngôn ngữ. Nó có đáng không? Tôi muốn nói rằng thực tế là tất cả các PL chính đều có phần mở rộng MP cho thấy rất nhiều lập trình viên đang làm việc thấy sự đánh đổi MP mang lại hữu ích.
Martin Berger

Câu trả lời:


10

Bạn có thể làm cho bất kỳ ngôn ngữ homoiconic. Về cơ bản, bạn làm điều này bằng cách 'phản chiếu' ngôn ngữ (nghĩa là đối với bất kỳ nhà xây dựng ngôn ngữ nào bạn thêm một đại diện tương ứng của nhà xây dựng đó dưới dạng dữ liệu, hãy nghĩ AST). Bạn cũng cần thêm một vài thao tác bổ sung như trích dẫn và bỏ phiếu. Đó là ít nhiều nó.

Lisp đã sớm có được điều đó vì cú pháp dễ dàng của nó, nhưng họ ngôn ngữ MetaML của W. Taha cho thấy rằng nó có thể làm được với bất kỳ ngôn ngữ nào.

Toàn bộ quá trình được phác thảo trong Mô hình hóa lập trình meta tổng quát đồng nhất . Một giới thiệu nhẹ hơn cho cùng một vật liệu ở đây .


1
Sửa lỗi cho tôi nếu tôi sai. "Phản chiếu" có liên quan đến phần thứ hai của câu hỏi (tính đồng âm trong io lang), phải không?
incud

@Ignus Tôi không chắc là tôi hoàn toàn hiểu nhận xét của bạn. Mục đích của homoiconicity là cho phép xử lý mã dưới dạng dữ liệu. Điều đó có nghĩa là bất kỳ dạng mã nào cũng phải có biểu diễn dưới dạng dữ liệu. Có một số cách để thực hiện việc này (ví dụ: AST gần như trích dẫn, sử dụng các loại để phân biệt mã với dữ liệu được thực hiện theo phương pháp phân tầng mô đun trọng lượng nhẹ), nhưng tất cả đều yêu cầu nhân đôi / phản chiếu cú ​​pháp ngôn ngữ ở một số dạng.
Martin Berger

Tôi cho rằng @Ignus sẽ có lợi khi nhìn vào MetaOCaml? Có phải là "homoiconic" chỉ có nghĩa là được trích dẫn sau đó? Tôi giả sử rằng các ngôn ngữ nhiều giai đoạn như MetaML và MetaOCaml đi xa hơn?
Steven Shaw

1
@StevenShaw MetaOCaml rất thú vị, đặc biệt là BER MetaOCaml mới của Oleg . Tuy nhiên, điều bị hạn chế ở chỗ nó chỉ thực hiện lập trình meta thời gian chạy và chỉ thể hiện mã thông qua các trích dẫn gần như không biểu cảm như AST.
Martin Berger

7

Trình biên dịch Ocaml được viết bằng chính Ocaml, vì vậy chắc chắn có một cách để thao túng Ocaml ASTs trong Ocaml.

Người ta có thể tưởng tượng việc thêm một loại tích ocaml_syntaxhợp vào ngôn ngữ và có một defmacrohàm tích hợp, có một đầu vào của loại, nói

f : ocaml_syntax -> ocaml_syntax

Bây giờ các loạidefmacrogì? Vâng, điều đó thực sự phụ thuộc vào đầu vào, vì ngay cả khi flà hàm nhận dạng, loại đoạn mã kết quả phụ thuộc vào đoạn cú pháp được truyền vào.

Vấn đề này không phát sinh trong lisp, vì ngôn ngữ được gõ động, và không có loại nào cần phải được gán cho chính macro trong thời gian biên dịch. Một giải pháp sẽ là có

defmacro : (ocaml_syntax -> ocaml_syntax) -> 'a

mà sẽ cho phép macro được sử dụng trong bất kỳ bối cảnh nào . Nhưng điều này là không an toàn, tất nhiên, nó sẽ cho phép boolsử dụng thay vì làm stringhỏng chương trình trong thời gian chạy.

Giải pháp nguyên tắc duy nhất trong ngôn ngữ gõ tĩnh sẽ là có các loại phụ thuộc trong đó loại kết quả defmacrosẽ phụ thuộc vào đầu vào. Mọi thứ trở nên khá phức tạp vào thời điểm này, và tôi sẽ bắt đầu bằng cách chỉ cho bạn vào luận văn tốt đẹp của David Raymond Christiansen.

Tóm lại: có cú pháp phức tạp không phải là vấn đề, vì có nhiều cách để biểu thị cú pháp bên trong ngôn ngữ và có thể sử dụng lập trình meta như một quotethao tác để nhúng cú pháp "đơn giản" vào bên trong ocaml_syntax.

Vấn đề là làm cho điều này được gõ tốt, đặc biệt là có một cơ chế macro thời gian chạy không cho phép lỗi loại.

Có một cơ chế thời gian biên dịch cho các macro trong một ngôn ngữ như Ocaml là điều tất nhiên, có thể xem, ví dụ như MetaOcaml .

Cũng có thể hữu ích: Jane street về lập trình meta trong Ocaml


2
MetaOCaml có lập trình meta-thời gian chạy, không phải lập trình meta thời gian biên dịch. Ngoài ra hệ thống gõ của MetaOCaml không có loại phụ thuộc. (MetaOCaml cũng được phát hiện là không có loại!) Mẫu Haskell có một cách tiếp cận trung gian thú vị: mọi giai đoạn đều an toàn về loại, nhưng khi bước vào giai đoạn mới, chúng ta phải thực hiện kiểm tra lại loại. Điều này hoạt động thực sự tốt trong thực tế theo kinh nghiệm của tôi và bạn không mất đi lợi ích của an toàn loại ở giai đoạn cuối (thời gian chạy).
Martin Berger

@cody có thể có siêu lập trình trong OCaml cũng với Điểm mở rộng , phải không?
incud

@Ignus Tôi e rằng tôi không biết nhiều về Điểm mở rộng, mặc dù tôi có tham khảo nó trong liên kết đến blog Jane Street.
cody

1
Trình biên dịch C của tôi được viết bằng C, nhưng điều đó không có nghĩa là bạn có thể thao tác AST trong C ...
BlueRaja - Danny Pflughoeft

2
@immibis: Rõ ràng, nhưng nếu đó là những gì anh ta muốn nói thì câu nói đó vừa trống rỗng vừa không liên quan đến câu hỏi ...
BlueRaja - Danny Pflughoeft

1

Như một ví dụ, hãy xem xét F # (dựa trên OCaml). F # không hoàn toàn đồng âm, nhưng hỗ trợ lấy mã của hàm dưới dạng AST trong một số trường hợp nhất định.

Trong F #, bạn printsẽ được thể hiện dưới Exprdạng:

Let (message, Value ("Hello world"), Call (None, print_endline, [message]))

Để làm nổi bật cấu trúc tốt hơn, đây là một cách khác để bạn có thể tạo tương tự Expr:

let messageVar = Var("message", typeof<string>)
let expr = Expr.Let(messageVar,
                    Expr.Value("Hello world"),
                    Expr.Call(print_endline_method, [Expr.Var(messageVar)]))

Tôi đã không hiểu nó. Bạn có nghĩa là F # cho phép bạn "xây dựng" AST của một biểu thức và sau đó thực hiện nó? Nếu vậy, sự khác biệt với các ngôn ngữ cho phép bạn sử dụng eval(<string>)chức năng là gì? ( Theo nhiều tài nguyên, có chức năng eval khác với việc có đồng âm - đó có phải là lý do tại sao bạn nói F # không hoàn toàn đồng âm?)
incud

@Ignus Bạn có thể tự xây dựng AST hoặc bạn có thể để trình biên dịch thực hiện. Homoiconicity "cho phép tất cả các mã trong ngôn ngữ được truy cập và chuyển đổi dưới dạng dữ liệu" . Trong F #, bạn có thể truy cập một số mã dưới dạng dữ liệu. (Ví dụ: bạn cần đánh dấu printbằng [<ReflectedDefinition>]thuộc tính.)
svick
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.