Linguaggi di programmazione con un meccanismo di estensione della sintassi simile a Lisp [chiuso]

20

Ho solo una conoscenza limitata di Lisp (cerco di imparare un po 'nel mio tempo libero) ma per quanto ne so io le macro Lisp permettono di introdurre costrutti e sintassi di un nuovo linguaggio descrivendoli in Lisp stesso. Ciò significa che un nuovo costrutto può essere aggiunto come una libreria, senza modificare il compilatore / interprete Lisp.

Questo approccio è molto diverso da quello di altri linguaggi di programmazione. Ad esempio, se volessi estendere Pascal con un nuovo tipo di loop o qualche particolare idioma, dovrei estendere la sintassi e la semantica del linguaggio e quindi implementare quella nuova funzionalità nel compilatore.

Esistono altri linguaggi di programmazione al di fuori della famiglia Lisp (ad eccezione di Common Lisp, Scheme, Clojure (?), Racket (?), ecc.) che offrono una possibilità simile di estendere la lingua all'interno della lingua stessa?

Modifica

Per favore, evita discussioni estese ed essere specifico nelle tue risposte. Invece di una lunga lista di linguaggi di programmazione che possono essere estesi in un modo o nell'altro, mi piacerebbe capire da un punto di vista concettuale cosa è specifico delle macro Lisp come meccanismo di estensione e quali linguaggi di programmazione non-Lisp offrono alcuni concetti è vicino a loro.

    
posta Giorgio 13.09.2012 - 11:54
fonte

19 risposte

19

Anche questo è reso possibile da Scala (infatti è stato progettato in modo consapevole per supportare la definizione di nuovi costrutti di linguaggio e persino per completare i DSL).

Oltre alle funzioni di ordine superiore, lambda e curring, che sono comuni nei linguaggi funzionali, ci sono alcune caratteristiche linguistiche speciali qui per abilitare questo *:

  • nessun operatore - tutto è una funzione, ma i nomi delle funzioni possono includere caratteri speciali come "+", "-" o ":"
  • i punti e le parentesi possono essere omessi per le chiamate al metodo a parametro singolo, ovvero a.and(b) è equivalente a a and b nel modulo infisso
  • per le chiamate con funzione a parametro singolo puoi usare parentesi graffe invece di parentesi graffe normali - questo (insieme al currying) ti permette di scrivere cose come

    val file = new File("example.txt")
    
    withPrintWriter(file) {
      writer => writer.println("this line is a function call parameter")
    }
    

    dove withPrintWriter è un metodo semplice con due elenchi di parametri, entrambi contenenti un singolo parametro

  • I parametri
  • per nome consentono di omettere l'elenco di parametri vuoto in lambdas, consentendo di scrivere chiamate come myAssert(() => x > 3) in un formato più breve come myAssert(x > 3)

La creazione di un esempio di DSL è discussa in dettaglio in Capitolo 11. Lingue specifiche del dominio in Scala del libro gratuito Programmazione Scala .

* Non intendo che siano unici per Scala, ma almeno non sembrano molto comuni. Però non sono un esperto di lingue funzionali.

    
risposta data 12.09.2012 - 22:36
fonte
13

Perl consente la pre-elaborazione della sua lingua. Anche se questo non è spesso usato al punto di cambiare radicalmente la sintassi nella lingua, può essere visto in alcuni dei ... moduli dispari:

Esiste anche un modulo che consente a perl di eseguire codice simile a come è stato scritto in Python.

Un approccio più moderno a questo all'interno di perl sarebbe utilizzare Filter :: Simple (uno dei principali moduli in perl5).

Si noti che tutti questi esempi coinvolgono Damian Conway che è stato definito il "Mad Doctor of Perl". È ancora un'abilità straordinariamente potente in Perl per distorcere il linguaggio come si desidera.

C'è più documentazione per questa e altre alternative su perlfilter .

    
risposta data 12.09.2012 - 23:07
fonte
13

Haskell

Haskell ha "Template Haskell" e "Quasiquotation":

link

link

Queste funzionalità consentono agli utenti di aggiungere in modo drammatico la sintassi della lingua al di fuori dei normali mezzi. Questi sono risolti anche in fase di compilazione, che ritengo sia un grosso must (almeno per le lingue compilate) [1].

Ho usato quasiquotation in Haskell una volta prima per creare un pattern matcher avanzato su un linguaggio simile a C:

moveSegment :: [Token] -> Maybe (SegPath, SegPath, [Token])
moveSegment [hc| HC_Move_Segment(@s, @s); | s1 s2 ts |] = Just (mkPath s1, mkPath s2, ts)
moveSegment _ = Nothing

[1] Altrimenti il seguente si qualifica come estensione della sintassi: runFeature "some complicated grammar enclosed in a string to be evaluated at runtime" , che ovviamente è un carico di merda.

    
risposta data 14.09.2012 - 09:16
fonte
12

Tcl ha una lunga storia di supporto della sintassi estensibile. Ad esempio, ecco l'implementazione di un loop che itera tre variabili (fino all'arresto) sui cardinali, i loro quadrati ei loro cubi:

proc loopCard23 {cardinalVar squareVar cubeVar body} {
    upvar 1 $cardinalVar cardinal $squareVar square $cubeVar cube

    # We borrow a 'for' loop for the implementation...
    for {set cardinal 0} true {incr cardinal} {
        set square [expr {$cardinal ** 2}]
        set cube [expr {$cardinal ** 3}]

        uplevel 1 $body
    }
}

Questo dovrebbe essere usato in questo modo:

loopCard23 a b c {
    puts "got triplet: $a, $b, $c"
    if {$c > 400} {
        break
    }
}

Questo tipo di tecnica è ampiamente utilizzata nella programmazione Tcl e la chiave per farlo in modo corretto sono i comandi upvar e uplevel ( upvar associa una variabile denominata in un altro ambito a una variabile locale e uplevel esegue uno script in un altro ambito: in entrambi i casi, 1 indica che l'ambito in questione è del chiamante). Viene anche usato molto nel codice che si accoppia con i database (eseguendo del codice per ogni riga in un set di risultati), in Tk per GUI (per eseguire il binding di callback agli eventi), ecc.

Tuttavia, questa è solo una piccola parte di ciò che viene fatto. Il linguaggio incorporato non ha nemmeno bisogno di essere Tcl; può essere praticamente qualsiasi cosa (purché equilibri le sue parentesi - le cose diventano sintatticamente orribili se ciò non è vero - che è l'enorme maggioranza dei programmi) e Tcl può semplicemente inviare alla lingua straniera incorporata, se necessario. Esempi di questa operazione includono l'incorporamento di C per implementare i comandi Tcl e l'equivalente con Fortran. (Probabilmente, tutti i comandi integrati di Tcl sono fatti in questo modo in un certo senso, in quanto sono in realtà solo una libreria standard e non la lingua stessa.)

    
risposta data 13.09.2012 - 10:49
fonte
10

Questa è in parte una questione di semantica. L'idea di base di Lisp è che il programma è costituito da dati che possono essere manipolati. Le lingue comunemente utilizzate nella famiglia Lisp, come Scheme, non consentono di aggiungere una nuova sintassi nel senso del parser; sono solo liste parentesi delimitate da spazi. È solo che, poiché la sintassi di base fa così poco, puoi farne quasi tutti i costrutti semantici . Scala (discussa di seguito) è simile: le regole dei nomi delle variabili sono così liberali che puoi facilmente creare dei bei DSL (rimanendo all'interno delle stesse regole di sintassi di base).

Queste lingue, anche se non consentono di definire una nuova sintassi nel senso dei filtri Perl, hanno un core sufficientemente flessibile da poter essere utilizzato per creare DSL e aggiungere costrutti di linguaggio.

L'importante caratteristica comune è che ti permettono di definire costrutti linguistici che funzionano così come quelli incorporati, utilizzando le funzionalità esposte dalle lingue. Il grado di supporto per questa funzione varia:

  • Molte lingue meno recenti hanno fornito funzioni integrate come sin() , round() , ecc., senza alcuno strumento per implementare le tue.
  • C ++ fornisce supporto limitato. Ad esempio alcune parole chiave integrate come cast ( static_cast<target_type>(input) , dynamic_cast<>() , const_cast<>() , reinterpret_cast<>() ) possono essere emulate usando le funzioni template, che Boost usa per lexical_cast<>() , polymorphic_cast<>() , any_cast<>() ,. ...
  • Java ha strutture di controllo integrate ( for(;;){} , while(){} , if(){}else{} , do{}while() , synchronized(){} , strictfp{} ) e non ti consente di definirne le tue. Scala definisce invece una sintassi astratta che consente di chiamare le funzioni utilizzando una sintassi simile alla struttura di controllo e le librerie utilizzano questo per definire efficacemente nuove strutture di controllo (ad esempio react{} nella libreria degli attori).

Inoltre, potresti esaminare la funzionalità di sintassi personalizzata di Mathematica nel pacchetto di notazione . (Tecnicamente è nella famiglia Lisp, ma ha alcune caratteristiche di estensibilità fatte in modo diverso, oltre alla solita estensibilità Lisp.)

    
risposta data 13.09.2012 - 11:02
fonte
8

Rebol suona quasi come quello che stai descrivendo, ma un po 'di lato.

Piuttosto che definire una sintassi specifica, tutto in Rebol è una chiamata di funzione - non ci sono parole chiave. (Sì, puoi ridefinire if e while se lo desideri veramente). Ad esempio, questa è un'istruzione if :

if now/time < 12:00 [print "Morning"]

if è una funzione che accetta 2 argomenti: una condizione e un blocco. Se la condizione è vera, il blocco viene valutato. Sembra la maggior parte delle lingue, giusto? Bene, il blocco è una struttura dati, non è limitato al codice: questo è un blocco di blocchi, ad esempio, e un rapido esempio della flessibilità di "codice è dati":

SomeArray: [ [foo "One"] [bar "Two"] [baz "Three"] ]
foreach action SomeArray [action/1: 'print] ; Change the data
if now/time < 12:00 SomeArray/2 ; Use the data as code - right now, if now/time < 12:00 [print "Two"]

Finché è possibile attenersi alle regole di sintassi, l'estensione di questa lingua è, per la maggior parte, non più che la definizione di nuove funzioni. Ad esempio, alcuni utenti hanno utilizzato le funzionalità di rebol 3 in Rebol 2, ad esempio.

    
risposta data 12.09.2012 - 22:41
fonte
7

Ruby ha una sintassi abbastanza flessibile, penso che sia un modo per "estendere la lingua all'interno della lingua stessa".

Un esempio è rake . È scritto in Ruby, è Ruby, ma sembra fare .

Per verificare alcune possibilità puoi cercare le parole chiave Ruby e metaprogramming .

    
risposta data 12.09.2012 - 22:23
fonte
7

Estendere la sintassi nel modo in cui parli ti consente di creare lingue specifiche del dominio. Quindi forse il il modo più utile per riformulare la tua domanda è, quali altre lingue hanno un buon supporto per le lingue specifiche del dominio?

Ruby ha una sintassi molto flessibile e molti DSL sono fioriti lì, come il rake. Groovy include molto di quella bontà. Include anche le trasformazioni AST, che sono più direttamente analoghe alle macro Lisp.

R, il linguaggio per il calcolo statistico, consente alle funzioni di ottenere i loro argomenti non valutati. Lo usa per creare un DSL per specificare la formula di regressione. Ad esempio:

y ~ a + b

significa "misura una linea del modulo k0 + k1 * a + k2 * b con i valori in y".

y ~ a * b

significa "misura una linea del modulo k0 + k1 * a + k2 * b + k3 * a * b con i valori in y".

E così via.

    
risposta data 12.09.2012 - 22:41
fonte
7

Converge è un altro linguaggio di metaprogrammazione non-lispy. E, in una certa misura, anche il C ++ si qualifica.

Probabilmente, MetaOCaml è abbastanza lontano da Lisp. Per uno stile completamente diverso di estensibilità della sintassi, ma abbastanza potente, dai un'occhiata a CamlP4 .

Nemerle è un altro linguaggio estensibile con metaprogrammazione in stile Lisp, anche se è più simile a lingue come Scala.

E, Scala stessa diventerà presto anche in questo linguaggio.

Modifica: ho dimenticato l'esempio più interessante: JetBrains MPS . Non è solo molto distante da tutto il Lispish, è anche un sistema di programmazione non testuale, con un editor che opera direttamente su un livello AST.

Modifica2: per rispondere a una domanda aggiornata - non c'è nulla di unico ed eccezionale nelle macro Lisp. In teoria, qualsiasi linguaggio può fornire un tale meccanismo (l'ho fatto anche con la semplice C). Tutto ciò di cui hai bisogno è un accesso al tuo AST e la possibilità di eseguire il codice in fase di compilazione. Qualche riflessione potrebbe essere d'aiuto (interrogare sui tipi, le definizioni esistenti, ecc.).

    
risposta data 13.09.2012 - 12:08
fonte
6

Prolog consente di definire nuovi operatori che vengono tradotti in termini composti con lo stesso nome. Ad esempio, questo definisce un operatore has_cat e lo definisce come un predicato per verificare se un elenco contiene l'atomo cat :

:- op(500, xf, has_cat).
X has_cat :- member(cat, X).

?- [apple, cat, orange] has_cat.
true ;
false.

Il xf significa che has_cat è un operatore postfisso; l'utilizzo di fx lo renderebbe un operatore di prefisso e xfx lo renderebbe un operatore infisso, con due argomenti. Controlla questo link per maggiori dettagli sulla definizione degli operatori in Prolog.

    
risposta data 12.09.2012 - 23:08
fonte
5

TeX è totalmente mancante dalla lista. Lo sapete tutti, giusto? Sembra qualcosa del genere:

Some {\it ''interesting''} example.

... tranne che è possibile ridefinire la sintassi senza restrizioni. Ogni token (!) Nella lingua può essere assegnato a un nuovo significato. ConTeXt è un pacchetto macro che ha sostituito le parentesi graffe con parentesi quadre:

Some \it[''interesting''] example.

Il pacchetto macro più comune LaTeX ridefinisce anche la lingua per i suoi scopi, ad es. aggiungendo la sintassi \begin{environment}…\end{environment} .

Ma non finisce qui. Tecnicamente, potresti anche ridefinire i token per analizzare quanto segue:

Some <it>“interesting”</it> example.

Sì, assolutamente possibile. Alcuni pacchetti usano questo per definire piccoli linguaggi specifici del dominio. Ad esempio, il pacchetto TikZ definisce una sintassi concisa per i disegni tecnici, che consente quanto segue:

\foreach \angle in {0, 30, ..., 330} 
  \draw[line width=1pt] (\angle:0.82cm) -- (\angle:1cm);

Inoltre, TeX è completo di Turing in modo che tu possa letteralmente fare tutto con esso. Non ho mai visto questo usato al massimo del suo potenziale perché sarebbe piuttosto inutile e molto contorto, ma è del tutto possibile rendere il seguente codice analizzabile semplicemente ridefinendo i token (ma questo probabilmente andrebbe ai limiti fisici del parser, a causa del come è costruito):

for word in [Some interesting example.]:
    if word == interesting:
        it(word)
    else:
        word
    
risposta data 13.09.2012 - 16:47
fonte
5

Boo ti consente di personalizzare notevolmente il linguaggio in fase di compilazione tramite macro sintattiche.

Boo ha una "pipeline estensibile per il compilatore". Ciò significa che il compilatore può chiamare il tuo codice per effettuare trasformazioni AST in qualsiasi momento durante la pipeline del compilatore. Come sai, cose come Java's Generics o C # 's Linq sono solo trasformazioni di sintassi in fase di compilazione, quindi questo è abbastanza potente.

Rispetto a Lisp, il vantaggio principale è che funziona con qualsiasi tipo di sintassi. Boo sta usando una sintassi ispirata a Python, ma potresti probabilmente scrivere un compilatore estensibile con una sintassi C o Pascal. E dal momento che la macro viene valutata al momento della compilazione, non vi è alcuna penalità sulle prestazioni.

I lati negativi, rispetto a Lisp, sono:

  • Lavorare con un AST non è elegante come lavorare con le espressioni s
  • Poiché la macro viene richiamata in fase di compilazione, non ha accesso ai dati di runtime.

Ad esempio, ecco come potresti implementare una nuova struttura di controllo:

macro repeatLines(repeatCount as int, lines as string*):
    for line in lines:
        yield [| print $line * $repeatCount |]

utilizzo:

repeatLines 2, "foo", "bar"

che poi viene tradotto, in fase di compilazione, in qualcosa del tipo:

print "foo" * 2
print "bar" * 2

(Sfortunatamente, la documentazione online di Boo è sempre irrimediabilmente obsoleta e non copre nemmeno cose avanzate come questa. La migliore documentazione per la lingua che conosco è questo libro: link )

    
risposta data 14.09.2012 - 11:13
fonte
4

La valutazione Mathematica si basa sulla corrispondenza e sostituzione dei pattern. Ciò consente di creare le proprie strutture di controllo, modificare le strutture di controllo esistenti o modificare il modo in cui le espressioni vengono valutate. Ad esempio, potresti implementare "logica fuzzy" come questa (un po 'semplificata):

fuzzy[a_ && b_]      := Min[fuzzy[a], fuzzy[b]]
fuzzy[a_ || b_]      := Max[fuzzy[a], fuzzy[b]]
fuzzy[!a_]           := 1-fuzzy[a]
If[fuzzy[a_], b_,c_] := fuzzy[a] * fuzzy[b] + fuzzy[!a] * fuzzy[c]

Questo sovrascrive la valutazione per gli operatori logici predefiniti & &, || ,! e la clausola If incorporata.

Puoi leggere queste definizioni come definizioni di funzioni, ma il vero significato è: se un'espressione corrisponde al modello descritto sul lato sinistro, viene sostituita con l'espressione sul lato destro. Potresti definire la tua clausola If come questa:

myIf[True, then_, else_] := then
myIf[False, then_, else_] := else
SetAttributes[myIf, HoldRest]

SetAttributes[..., HoldRest] indica al valutatore che dovrebbe valutare il primo argomento prima della corrispondenza del modello, ma mantenere la valutazione per il resto fino a quando il modello non è stato abbinato e sostituito.

Questo è ampiamente utilizzato all'interno delle librerie standard Mathematica ad es. definisci una funzione D che prende un'espressione e valuta la sua derivata simbolica.

    
risposta data 13.09.2012 - 11:33
fonte
3

Metalua è un linguaggio e un compilatore compatibile con Lua che fornisce questo.

  • Full compatibility with Lua 5.1 sources and bytecode: clean, elegant semantics and syntax, amazing expressive power, good performances, near-universal portability. - A complete macro system, similar in power to what's offfered by Lisp dialects or Template Haskell; manipulated programs can be seen
    as source code, as abstract syntax trees, or as an arbitrary mix
    thereof, whichever suits your task better.
  • A dynamically extensible parser, which lets you support your macros with a syntax that blends nicely with the rest of the language.

  • A set of language extensions, all implemented as regular metalua macros.

Differenze con Lisp:

  • Don't bother developers with macros when they aren't writing one: the language's syntax and semantics should be best suited for those 95% of the time when we aren't writing macros.
  • Encourage developers to follow the conventions of the language: not only with "best practices rants" nobody listen to, but by offering an API that makes it easier to write things the Metalua Way. Readability by fellow developers is more important and more difficult to achieve than readability by compilers, and for this, having a common set of respected conventions helps a lot.
  • Yet provide all the power one's willing to handle. Neither Lua nor Metalua are into mandatory bondage and discipline, so if you know what you're doing, the language won't get in your way.
  • Make it obvious when something interesting happens: all meta-operations happen between +{...} and -{...}, and visually stick out of the regular code.

Un esempio di applicazione è l'implementazione della corrispondenza del modello di tipo ML.

Vedi anche: link

    
risposta data 13.09.2012 - 07:04
fonte
2

Se stai cercando lingue estendibili, dovresti dare un'occhiata a Smalltalk.

In Smalltalk, il solo modo per programmare è di estendere effettivamente la lingua. Non c'è differenza tra l'IDE, le librerie o il linguaggio stesso. Sono tutti così intrecciati che Smalltalk viene spesso definito un ambiente piuttosto che una lingua.

Non scrivi applicazioni autonome in Smalltalk, ma estendi l'ambiente della lingua.

Controlla link per una manciata di risorse e informazioni.

Mi piacerebbe raccomandare Pharo come il dialetto dell'ingresso nel mondo di Smalltalk: link

Spero che sia di aiuto!

    
risposta data 13.09.2012 - 13:57
fonte
1

Ci sono strumenti che consentono di creare linguaggi personalizzati senza scrivere un intero compilatore da zero. Ad esempio c'è Spoofax , che è uno strumento di trasformazione del codice: inserisci regole grammaticali e di trasformazione (scritte in un modo dichiarativo di alto livello ), e quindi puoi generare il codice sorgente Java (o altra lingua, se ti interessa abbastanza) da un linguaggio personalizzato progettato da te.

Quindi, sarebbe possibile prendere la grammatica della lingua X, definire la grammatica del linguaggio X '(X con le estensioni personalizzate) e la trasformazione X' → X, e Spoofax genererà un compilatore X '→ X.

Attualmente, se ho capito bene, il supporto migliore è per Java, con il supporto C # in fase di sviluppo (o così ho sentito). Questa tecnica potrebbe essere applicata a qualsiasi lingua con grammatica statica (quindi, ad esempio probabilmente non Perl ).

    
risposta data 13.09.2012 - 03:20
fonte
1

Forth è un'altra lingua che è altamente estensibile. Molte implementazioni di Forth sono costituite da un piccolo kernel scritto in assembler o C, quindi il resto della lingua è scritto in Forth stesso.

Ci sono anche diversi linguaggi basati su stack che sono ispirati a Forth e condividono questa funzione, come Fattore .

    
risposta data 14.09.2012 - 12:25
fonte
0

Funge-98

La funzione di impronta digitale di Funge-98 consente di eseguire una ristrutturazione completa dell'intera sintassi e della semantica della lingua. Ma solo se l'implementatore fornisce un meccanismo di impronte digitali che ha permesso all'utente di modificare il langauage in modo programmatico (ciò è teoricamente possibile implementarlo nella normale sintassi e semantica del Funge-98). Se è così, si potrebbe letteralmente fare il resto del file (o qualsiasi parte del file) come C ++ o Lisp (o qualsiasi altra cosa voglia).

link

    
risposta data 14.09.2012 - 09:17
fonte
-1

Per ottenere ciò che stai cercando hai davvero bisogno di quelle parentesi e di una mancanza di sintassi. Alcuni linguaggi basati sulla sintassi potrebbero avvicinarsi, ma non è esattamente la stessa cosa di una vera macro.

    
risposta data 13.09.2012 - 21:10
fonte

Leggi altre domande sui tag