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
Odpowiedzi:
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 .
źródło
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_syntax
do języka i posiadaniedefmacro
wbudowanej funkcji, która wymaga wprowadzenia typu, powiedzmyTeraz jaki jest typ z
defmacro
? Cóż, tak naprawdę zależy to od danych wejściowych, ponieważ nawet jeślif
jest 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ć
co pozwoliłoby na użycie makra w dowolnym kontekście. Ale jest to oczywiście niebezpieczne, pozwoliłoby na użycie a
bool
zamiaststring
awarii 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 wyniku
defmacro
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ętrznejocaml_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
źródło
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 #
print
będzie reprezentowany jakoExpr
wydrukowany jako:Aby lepiej podkreślić strukturę, oto alternatywny sposób, w jaki możesz to zrobić
Expr
:źródło
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?)print
za pomocą[<ReflectedDefinition>]
atrybutu.)