Jak zrobić język homoiconic


16

Zgodnie z tym artykułem następujący wiersz kodu Lisp wypisuje „Hello world” na standardowe wyjście.

(format t "hello, world")

Lisp, który jest językiem homoiconic , może traktować kod jako dane w następujący sposób:

Teraz wyobraź sobie, że napisaliśmy następujące makro:

(defmacro backwards (expr) (reverse expr))

wstecz to nazwa makra, która przyjmuje wyrażenie (reprezentowane jako lista) i odwraca je. Oto znowu „Witaj, świecie”, tym razem za pomocą makra:

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

Gdy kompilator Lisp widzi ten wiersz kodu, patrzy na pierwszy atom na liście ( backwards) i zauważa, że ​​nazywa makro. Przekazuje nieocenioną listę ("hello, world" t format)do makra, które przestawia listę na (format t "hello, world"). Wynikowa lista zastępuje wyrażenie makr i jest to, co zostanie ocenione w czasie wykonywania. Środowisko Lisp zobaczy, że jego pierwszy atom ( format) jest funkcją, i oceni go, przekazując mu pozostałe argumenty.

W Lisp realizacja tego zadania jest łatwa (popraw mnie, jeśli się mylę), ponieważ kod jest implementowany jako lista ( wyrażenia s ?).

Teraz spójrz na ten fragment OCaml (który nie jest językiem homoiconic):

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

Wyobraź sobie, że chcesz dodać homoikoniczność do OCaml, który używa znacznie bardziej złożonej składni w porównaniu do Lisp. Jak byś to zrobił? Czy język musi mieć szczególnie łatwą składnię, aby osiągnąć homoikoniczność?

EDYCJA : w tym temacie znalazłem inny sposób osiągnięcia homoikoniczności, który różni się od Lispa: ten wdrożony w języku io . Może częściowo odpowiedzieć na to pytanie.

Tutaj zacznijmy od prostego bloku:

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

Okej, więc blok działa. Blok plus dodał dwie liczby.

A teraz zróbmy introspekcję na tym małym człowieku.

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
==> +

Gorąca święta zimna pleśń. Nie tylko możesz uzyskać nazwy parametrów bloku. Nie tylko możesz uzyskać ciąg pełnego kodu źródłowego bloku. Możesz zakraść się do kodu i przeglądać wiadomości w nim zawarte. I najbardziej niesamowite ze wszystkich: jest to okropnie łatwe i naturalne. Wierny zadaniu Io. Lustro Ruby tego nie widzi.

Ale, zaraz, hej, nie dotykaj tej tarczy.

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

1
Możesz rzucić okiem na to, jak Scala zrobił swoje makra
Bergi

1
@Bergi Scala ma nowe podejście do makr: scala.meta .
Martin Berger,

Zawsze uważałem, że homoiconicity jest przereklamowany. W każdym wystarczająco wydajnym języku zawsze możesz zdefiniować strukturę drzewa, która odzwierciedla strukturę samego języka, a funkcje narzędziowe można napisać, aby tłumaczyć na język źródłowy (i / lub skompilowaną formę) zgodnie z wymaganiami. Tak, jest to nieco łatwiejsze w LISP, ale biorąc pod uwagę, że (a) znaczna większość prac programistycznych nie powinna być metaprogramowaniem, oraz (b) LISP poświęcił jasność języka, aby było to możliwe, nie sądzę, aby kompromis był tego wart.
Periata Breatta

@PeriataBreatta Masz rację, ale główną zaletą MP jest to, że MP umożliwia abstrakcje bez kary czasu działania . W ten sposób MP rozwiązuje napięcie między abstrakcją a wydajnością, aczkolwiek kosztem rosnącej złożoności języka. Czy warto? Powiedziałbym, że fakt, że wszystkie główne PL mają rozszerzenia MP wskazuje, że wielu pracujących programistów uważa kompromisy za przydatne w MP.
Martin Berger,

Odpowiedzi:


10

Możesz ustawić dowolny język jako homoiconic. Zasadniczo robisz to poprzez „odbicie lustrzane” języka (co oznacza, że ​​dla dowolnego konstruktora języka dodajesz odpowiednią reprezentację tego konstruktora jako danych, pomyśl AST). Musisz także dodać kilka dodatkowych operacji, takich jak cytowanie i cofanie cudzysłowu. To mniej więcej tyle.

Lisp miał to wcześnie ze względu na łatwą składnię, ale rodzina języków MetaML W. Taha pokazała, że ​​można to zrobić w każdym języku.

Cały proces przedstawiono w Modelowaniu jednorodnego generatywnego metaprogramowania . Bardziej lekkie wprowadzenie do tego samego materiału jest tutaj .


1
Popraw mnie, jeśli się mylę. „mirroring” jest związany z drugą częścią pytania (homoiconicity in io lang), prawda?
tym

@Ignus Nie jestem pewien, czy w pełni rozumiem twoją uwagę. Celem homoikoniczności jest umożliwienie traktowania kodu jako danych. Oznacza to, że każda forma kodu musi mieć reprezentację jako dane. Można to zrobić na kilka sposobów (np. Quasi-cudzysłowy ASTs, używając typów do odróżnienia kodu od danych, jak to zrobiono przy zastosowaniu lekkiego modułowego stopniowania etapowego), ale wszystkie wymagają podwojenia / dublowania składni języka w jakiejś formie.
Martin Berger,

Zakładam, że @Ignus skorzystałby wtedy na MetaOCaml? Czy zatem bycie „homoikonicznym” oznacza po prostu cytowanie? Zakładam, że wieloetapowe języki, takie jak MetaML i MetaOCaml idą dalej?
Steven Shaw,

1
@StevenShaw MetaOCaml jest bardzo interesujący, szczególnie nowy BER MetaOCaml Olega . Jest jednak nieco ograniczony, ponieważ wykonuje tylko meta-programowanie w czasie wykonywania i reprezentuje kod tylko za pomocą quasi-cudzysłowów, co nie jest tak wyraziste jak AST.
Martin Berger,

7

Kompilator Ocaml jest napisany w samym Ocaml, więc na pewno istnieje sposób na manipulowanie Ocaml ASTs w Ocaml.

Można sobie wyobrazić dodanie wbudowanego typu ocaml_syntaxdo języka i posiadanie defmacrowbudowanej funkcji, która wymaga wprowadzenia typu, powiedzmy

f : ocaml_syntax -> ocaml_syntax

Teraz jaki jest typ z defmacro? Cóż, tak naprawdę zależy to od danych wejściowych, ponieważ nawet jeśli fjest to funkcja tożsamości, rodzaj wynikowego fragmentu kodu zależy od przekazanej składni.

Ten problem nie pojawia się w lisp, ponieważ język jest dynamicznie wpisywany i nie trzeba przypisywać żadnego typu do makra w czasie kompilacji. Jednym rozwiązaniem byłoby mieć

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

co pozwoliłoby na użycie makra w dowolnym kontekście. Ale jest to oczywiście niebezpieczne, pozwoliłoby na użycie a boolzamiast stringawarii programu w czasie wykonywania.

Jedynym zasadniczym rozwiązaniem w języku o typie statycznym byłoby posiadanie typów zależnych, w których typ wynikudefmacro byłby zależny od danych wejściowych. W tym momencie sprawy się komplikują i zacznę od wskazania miłej rozprawy Davida Raymonda Christiansena.

Podsumowując: skomplikowana składnia nie stanowi problemu, ponieważ istnieje wiele sposobów reprezentowania składni w języku i możliwe użycie metaprogramowania jak quote operacji do osadzenia „prostej” składni w wewnętrznej ocaml_syntax.

Problem polega na tym, że ten typ jest dobrze wpisany, w szczególności mając mechanizm makr w czasie wykonywania, który nie dopuszcza błędów typu.

Posiadanie kompilacji mechanizm makr w języku jak SML jest możliwe oczywiście, patrz np MetaOcaml .

Również prawdopodobnie przydatne: Jane Street na metaprogramowaniu w Ocaml


2
MetaOCaml ma meta-programowanie w czasie wykonywania, a nie meta-programowanie w czasie kompilacji. Również system pisania MetaOCaml nie ma typów zależnych. (Stwierdzono również, że MetaOCaml jest nieuzasadniony!) Szablon Haskell ma ciekawe podejście pośrednie: każdy etap jest bezpieczny dla typu, ale wchodząc w nowy etap, musimy ponownie sprawdzić typ. Z mojego doświadczenia działa to bardzo dobrze w praktyce i nie tracisz korzyści związanych z bezpieczeństwem typu na etapie końcowym (w czasie wykonywania).
Martin Berger,

@ Cody można mieć metaprogramowanie w OCaml również z punktami rozszerzeń , prawda?
tym

@Ignus Obawiam się, że niewiele wiem o punktach rozszerzeń, choć odsyłam do tego w linku do bloga Jane Street.
cody

1
Mój kompilator C jest napisany w C, ale to nie znaczy, że możesz manipulować AST w C ...
BlueRaja - Danny Pflughoeft,

2
@immibis: Oczywiście, ale jeśli o to mu chodziło, to stwierdzenie jest zarówno puste, jak i niezwiązane z pytaniem ...
BlueRaja - Danny Pflughoeft

1

Jako przykład rozważmy F # (oparty na OCaml). F # nie jest w pełni homoikoniczny, ale w pewnych okolicznościach obsługuje pobieranie kodu funkcji jako AST.

W F # printbędzie reprezentowany jako Exprwydrukowany jako:

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

Aby lepiej podkreślić strukturę, oto alternatywny sposób, w jaki możesz to zrobić Expr:

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

Nie zrozumiałam tego. Masz na myśli, że F # pozwala „zbudować” wyrażenie AST, a następnie je wykonać? Jeśli tak, to jaka jest różnica między językami, która pozwala korzystać z tej eval(<string>)funkcji? ( Według wielu źródeł posiadanie funkcji eval różni się od homoikoniczności - czy to jest powód, dla którego powiedziałeś, że F # nie jest w pełni homoikoniczny?)
tym

@Ignus Możesz zbudować AST samodzielnie lub pozwolić kompilatorowi to zrobić. Homoiconicity „umożliwia dostęp do całego kodu w języku i przekształcanie go jako dane” . W F # można uzyskać dostęp do niektórych kodów jako danych. (Na przykład musisz zaznaczyć printza pomocą [<ReflectedDefinition>]atrybutu.)
svick
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.