Come rendere un linguaggio omoiconico


16

Secondo questo articolo la seguente riga di codice Lisp stampa "Hello world" sull'output standard.

(format t "hello, world")

Lisp, che è un linguaggio omoiconico , può trattare il codice come dati in questo modo:

Ora immagina di aver scritto la seguente macro:

(defmacro backwards (expr) (reverse expr))

all'indietro è il nome della macro, che accetta un'espressione (rappresentata come un elenco) e la inverte. Ecco di nuovo "Ciao, mondo", questa volta usando la macro:

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

Quando il compilatore Lisp vede quella riga di codice, osserva il primo atomo nell'elenco ( backwards) e nota che nomina una macro. Passa l'elenco non valutato ("hello, world" t format)alla macro, che riorganizza l'elenco in (format t "hello, world"). L'elenco risultante sostituisce l'espressione macro ed è ciò che verrà valutato in fase di esecuzione. L'ambiente Lisp vedrà che il suo primo atomo ( format) è una funzione e lo valuterà, passandogli il resto degli argomenti.

Raggiungere questo compito in Lisp è facile (correggimi se sbaglio) perché il codice è implementato come elenco ( s-espressioni ?).

Ora dai un'occhiata a questo frammento di OCaml (che non è un linguaggio omoiconico):

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

Immagina di voler aggiungere l'omoiconicità a OCaml, che utilizza una sintassi molto più complessa rispetto a Lisp. Come lo faresti? La lingua deve avere una sintassi particolarmente semplice per ottenere l'omonicità?

EDIT : da questo argomento ho trovato un altro modo per ottenere l'omoiconicità che è diverso da quello di Lisp: quello implementato nel linguaggio io . Potrebbe parzialmente rispondere a questa domanda.

Qui, cominciamo con un semplice blocco:

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

Va bene, quindi il blocco funziona. Il blocco positivo ha aggiunto due numeri.

Ora facciamo un po 'di introspezione su questo ometto.

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

Muffa calda e fredda. Non solo puoi ottenere i nomi dei parametri di blocco. E non solo puoi ottenere una stringa del codice sorgente completo del blocco. Puoi intrufolarti nel codice e attraversare i messaggi all'interno. E la cosa più sorprendente di tutte: è tremendamente facile e naturale. Fedele alla ricerca di Io. Lo specchio di Ruby non riesce a vedere nulla di tutto ciò.

Ma, whoa whoa, hey ora, non toccare quel quadrante.

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

1
Potresti dare un'occhiata a come Scala ha fatto i suoi macronutrienti
Bergi,

1
@Bergi Scala ha un nuovo approccio alle macro: scala.meta .
Martin Berger,

Ho sempre pensato che l'omoiconicità sia sopravvalutata. In qualsiasi linguaggio sufficientemente potente è sempre possibile definire una struttura ad albero che rispecchia la struttura della lingua stessa e le funzioni di utilità possono essere scritte per tradurre da e verso la lingua di origine (e / o un modulo compilato) come richiesto. Sì, è leggermente più facile nei LISP, ma dato che (a) la maggior parte del lavoro di programmazione dovrebbe non essere metaprogrammazione e (b) LISP ha sacrificato la chiarezza del linguaggio per renderlo possibile, non credo che ne sia valsa la pena.
Periata Breatta,

@PeriataBreatta Hai ragione, ma il vantaggio principale di MP è che MP consente astrazioni senza penalità di runtime . Pertanto MP risolve la tensione tra astrazione e prestazioni, anche se a costo di aumentare la complessità del linguaggio. Ne vale la pena? Direi che il fatto che tutti i principali PL abbiano estensioni MP indica che molti programmatori che lavorano ritengono utili le offerte di MP di compromessi.
Martin Berger,

Risposte:


10

Puoi rendere qualsiasi lingua omoiconica. Fondamentalmente lo fai "rispecchiando" il linguaggio (nel senso che per ogni costruttore di linguaggi aggiungi una rappresentazione corrispondente di quel costruttore come dati, pensa AST). È inoltre necessario aggiungere un paio di operazioni aggiuntive come la citazione e l'annullamento della quotazione. È più o meno.

Lisp lo aveva fatto presto grazie alla sua sintassi semplice, ma la famiglia di lingue MetaML di W. Taha ha dimostrato che è possibile farlo per qualsiasi lingua.

L'intero processo è delineato nella modellizzazione della meta-programmazione generativa omogenea . Un'introduzione più leggera allo stesso materiale è qui .


1
Correggimi se sbaglio. Il "mirroring" è legato alla seconda parte della domanda (omoiconicità in io lang), giusto?
incud

@Ignus Non sono sicuro di aver compreso appieno la tua osservazione. Lo scopo dell'omoiconicità è di consentire il trattamento del codice come dati. Ciò significa che qualsiasi forma di codice deve avere una rappresentazione come dati. Esistono diversi modi per farlo (ad esempio quasi virgolette AST, utilizzando tipi per distinguere il codice dai dati come fatto dall'approccio di stadiazione modulare leggero), ma tutti richiedono un raddoppio / mirroring della sintassi del linguaggio in qualche forma.
Martin Berger,

Presumo che @Ignus trarrebbe beneficio dall'osservare MetaOCaml allora? Essere "omoiconico" significa solo essere quotabile allora? Suppongo che linguaggi multi-stage come MetaML e MetaOCaml vadano oltre?
Steven Shaw,

1
@StevenShaw MetaOCaml è molto interessante, in particolare il nuovo BER MetaOCaml di Oleg . Tuttavia, è in qualche modo limitato in quanto esegue solo meta-programmazione runtime e rappresenta il codice solo tramite quasi virgolette che non è espressivo come gli AST.
Martin Berger,

7

Il compilatore Ocaml è scritto in Ocaml stesso, quindi sicuramente esiste un modo per manipolare gli AST Ocaml in Ocaml.

Si potrebbe immaginare di aggiungere un tipo incorporato ocaml_syntax alla lingua e di avere una defmacrofunzione integrata, che accetta un input di tipo, ad esempio

f : ocaml_syntax -> ocaml_syntax

Ora qual è il tipo di defmacro? Bene, ciò dipende davvero dall'input, come anche se fè la funzione di identità, il tipo del pezzo di codice risultante dipende dal pezzo di sintassi trasmesso.

Questo problema non si presenta in lisp, poiché la lingua è tipizzata in modo dinamico e non è necessario attribuire alcun tipo alla macro stessa al momento della compilazione. Una soluzione sarebbe quella di avere

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

che consentirebbe di utilizzare la macro in qualsiasi contesto. Ma questo non è sicuro, ovviamente, permetterebbe booldi essere usato al posto di a string, facendo crashare il programma in fase di esecuzione.

L'unica soluzione di principio in un linguaggio tipicamente statico sarebbe quella di avere tipi dipendenti in cui il tipo di risultato didefmacro dipenderebbe dall'input. Le cose a questo punto diventano piuttosto complicate, e vorrei iniziare indicandoti la bella tesi di David Raymond Christiansen.

In conclusione: avere una sintassi complicata non è un problema, poiché ci sono molti modi per rappresentare la sintassi all'interno del linguaggio e possibilmente usare la meta-programmazione come un quote un'operazione per incorporare la sintassi "semplice" all'interno ocaml_syntax.

Il problema è renderlo ben tipizzato, in particolare con un meccanismo macro di runtime che non consente errori di tipo.

Ovviamente è possibile avere un meccanismo di compilazione per le macro in un linguaggio come Ocaml, vedi ad es MetaOcaml .

Forse anche utile: Jane Street su meta-programmazione in Ocaml


2
MetaOCaml ha una meta-programmazione runtime, non una meta-programmazione in fase di compilazione. Anche il sistema di tipizzazione di MetaOCaml non ha tipi dipendenti. (MetaOCaml è stato anche trovato insipido!) Template Haskell ha un approccio intermedio interessante: ogni fase è sicura, ma quando si entra in una nuova fase, è necessario ripetere il controllo del tipo. Funziona davvero bene nella mia esperienza e non perdi i benefici della sicurezza del tipo nella fase finale (tempo di esecuzione).
Martin Berger,

@cody è possibile avere metaprogrammazione in OCaml anche con Extension Point , giusto?
incud

@Ignus Temo di non sapere molto sui punti di estensione, anche se faccio riferimento nel link al blog di Jane Street.
cody

1
Il mio compilatore C è scritto in C, ma ciò non significa che puoi manipolare l'AST in C ...
BlueRaja - Danny Pflughoeft

2
@immibis: Ovviamente, ma se è quello che intendeva dire, quell'affermazione è sia vacua che estranea alla domanda ...
BlueRaja - Danny Pflughoeft

1

Ad esempio, considerare F # (basato su OCaml). F # non è completamente omoiconico, ma supporta l'ottenimento del codice di una funzione come AST in determinate circostanze.

In F #, il tuo printsarebbe rappresentato come Exprstampato come:

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

Per evidenziare meglio la struttura, ecco un modo alternativo per creare lo stesso Expr:

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

Non l'ho capito. Vuoi dire che F # ti permette di "costruire" l'AST di un'espressione e quindi eseguirla? In tal caso, qual è la differenza con le lingue che ti consentono di utilizzare la eval(<string>)funzione? ( Secondo molte risorse, avere la funzione eval è diverso dall'omogeneità - è la ragione per cui hai detto che F # non è completamente omoiconico?)
incud

@Ignus Puoi creare l'AST da solo o lasciare che sia il compilatore a farlo. L'omoiconicità "consente di accedere e trasformare tutti i dati nella lingua come dati" . In F #, puoi accedere ad alcuni codici come dati. (Ad esempio, è necessario contrassegnare printcon l' [<ReflectedDefinition>]attributo.)
svick
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.