web-dev-qa-db-fra.com

Programmation des langages avec un mécanisme d'extension de syntaxe de type LISP

Je n'ai qu'une connaissance limitée de LISP (essayant d'apprendre un peu dans mon temps libre), mais dans la mesure où je comprends bien, les macros LISP permettent d'introduire de nouvelles constructions de langue et de nouvelles syntaxes en les décrivant dans Lisp elle-même. Cela signifie qu'une nouvelle construction peut être ajoutée en tant que bibliothèque, sans changer le compilateur/interprète LISP.

Cette approche est très différente de celle des autres langages de programmation. E.G., Si je voulais étendre Pascal avec un nouveau type de boucle ou un idiome particulier, je devrais étendre la syntaxe et la sémantique de la langue, puis mettre en œuvre cette nouvelle fonctionnalité dans le compilateur.

Existe-t-il d'autres langages de programmation en dehors de la famille LISP (c'est-à-dire en dehors de la LISP commune, schéma, clojure (?), Raquette (?), Etc.), qui offrent une possibilité similaire d'étendre la langue dans la langue elle-même?

ÉDITER

S'il vous plaît, évitez la discussion prolongée et être spécifique dans vos réponses. Au lieu d'une longue liste de langages de programmation pouvant être étendus d'une manière ou d'une autre, je voudrais comprendre à partir d'un point de vue conceptuel ce qui est spécifique aux macros LISP en tant que mécanisme d'extension et quelles langues de programmation non LISP offrent un certain concept c'est proche d'eux.

20
Giorgio

Scala rend cela possible aussi (en fait, il était consciemment conçu pour soutenir la définition de nouvelles constructions linguistiques et même des DSL complètes).

Outre les fonctions supérieures d'ordre, les Lambdas et Currying, qui sont courantes dans des langages fonctionnelles, certaines fonctionnalités de langue spéciale sont ici pour permettre cela *:

  • aucun opérateur - Tout est une fonction, mais les noms de fonction peuvent inclure des caractères spéciaux tels que '+', '-' ou ':'
  • les points et les accolades peuvent être omis pour les appels de méthode de paramètres mono-paramètres, c'est-à-dire a.and(b) équivalent à a and b dans le formulaire Infix
  • pour les appels de fonctions à paramètres unique, vous pouvez utiliser des supports bouclés au lieu d'accolades normales - ceci (avec Currying) vous permet d'écrire des choses comme

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

    withPrintWriter est une méthode unie avec deux listes de paramètres, les deux contenant un seul paramètre

  • paramètres de noms vous permet d'omettre la liste de paramètres vide dans Lambdas, vous permettant d'écrire des appels comme myAssert(() => x > 3) dans une forme plus courte comme myAssert(x > 3)

La création d'un exemple DSL est discutée en détail dans Chapitre 11. Langues spécifiques au domaine dans Scala du livre gratuit Scala .

*Je ne veux pas dire que ce sont uniques à Scala, mais au moins ils ne semblent pas être très communs. Je ne suis pas un expert en langues fonctionnelles cependant.

19
Péter Török

Haskell

Haskell a "Modèle Haskell" ainsi que la "quasiiquotation":

http://www.hakell.org/hakellwiki/template_hakell

http://www.hakell.org/hakellwiki/quasiquotation

Ces fonctionnalités permettent aux utilisateurs d'ajouter de manière spectaculaire à la syntaxe de la langue en dehors des moyens normaux. Celles-ci sont résolues au moment de la compilation, ce qui, à mon avis, est un must (pour les langages compilés au moins) [1].

J'ai déjà utilisé la quasiiquotation dans Haskell une fois avant de créer un correspondant de modèle avancé sur une langue de type 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] Sinon, le suivant se qualifie comme poste de syntaxe: runFeature "some complicated grammar enclosed in a string to be evaluated at runtime", lequel est bien sûr une charge de merde.

13
Thomas Eding

Perl permet le prétraitement de sa langue. Bien que cela ne soit souvent utilisé dans l'étendue de la syntaxe de la langue dans la langue, on peut le voir dans certains des modules étranges:

  • ACME :: Bleach pour un code vraiment propre
  • ACME :: morse pour écrire en code morse
  • Lingua :: Romana :: perligata Pour écrire en latin (pour des exemples, les noms sont des variables et un numéro, une affaire et une déclinaison changent le type de nom Nountum ==> $ suivant, nexta ==> @ Suivant)

Il existe également un module qui permet à Perl d'exécuter du code qui semble être écrit en python.

Une approche plus moderne de cela au sein de Perl serait d'utiliser filter :: simple (l'un des modules principaux de Perl5).

Notez que tous ces exemples impliquent Damian Conway qui a été appelé "Mad Docteur de Perl". C'est toujours une capacité incroyablement puissante au sein de Perl pour tordre la langue comment on le veut.

Plus de documentation pour cela et d'autres alternatives existent à Perlfilter .

13
user40980

TCL a une longue histoire de soutenir la syntaxe extensible. Par exemple, voici la mise en œuvre d'une boucle qui compte trois variables (jusqu'à ce qu'elle soit arrêtée) sur les cardinaux, leurs carrés et leurs cubes:

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
    }
}

Ce serait alors utilisé comme ceci:

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

Ce type de technique est largement utilisé dans la programmation TCL et la clé pour le faire sanely sont les commandes upvar et uplevel (upvar lie une variable nommée dans une autre portée à variable locale, et uplevel exécute un script dans une autre portée: dans les deux cas, le 1 indique que la portée en question est l'appelant). Il est également utilisé beaucoup en code qui couples avec des bases de données (exécutant un certain code pour chaque ligne d'un ensemble de résultats), en TK pour Guis (pour la liaison des rappels aux événements), etc.

Cependant, ce n'est qu'une fraction de ce qui est fait. La langue intégrée n'a même pas besoin d'être TCL; Cela peut être pratiquement n'importe quoi (tant qu'il équilibre ses bretelles - les choses sont syntaxiquement horribles si ce n'est pas vrai - qui est l'énorme majorité des programmes) et TCL ne peut simplement envoyer à la langue étrangère intégrée si nécessaire. Des exemples de cela incluent incorporer C pour implémenter des commandes TCL et l'équivalent avec Fortran. (On peut soutenir que toutes les commandes intégrées de TCL sont effectuées de cette manière dans un sens, en ce sens qu'elles sont vraiment une bibliothèque standard et non la langue elle-même.)

12
Donal Fellows

C'est en partie une question de sémantique. L'idée de base de LISP est que le programme est des données pouvant être manipulées. Les langues couramment utilisées dans la famille LISP, comme le schéma, ne vous laissez pas vraiment ajouter de nouveau Syntaxe dans le sens de l'analyseur; Ce sont toutes des listes de parenthèses à la fois délimitées spatiales. C'est juste que puisque la syntaxe principale le fait si peu, vous pouvez faire presque n'importe quel sémantique Construire de celui-ci. =Scala (décrit ci-dessous) est similaire: les règles de nom variable sont si libérales que vous pouvez facilement faire de belles DSLS en dehors (tout en restant dans les mêmes règles de syntaxe de base).

Ces langues, bien qu'ils ne vous laissent pas réellement définir la nouvelle syntaxe dans le sens des filtres PERL, ont un noyau suffisamment flexible que vous pouvez l'utiliser pour créer des DSLS et ajouter des constructions de langue.

La caractéristique commune importante est qu'elles vous permettent de définir des constructions linguistiques qui fonctionnent ainsi que des caractéristiques exposées par les langues. Le degré de support pour cette fonctionnalité varie:

  • De nombreuses langues plus anciennes ont fourni des fonctions intégrées telles que sin(), round(), etc., sans aucun moyen de mettre en œuvre votre propre.
  • C++ fournit un support limité. Par exemple, certains mots-clés intégrés tels que CASTS (static_cast<target_type>(input), dynamic_cast<>(), const_cast<>(), reinterpret_cast<>()) peut être émulée à l'aide de fonctions de modèle, qui Boost utilise pour lexical_cast<>(), polymorphic_cast<>(), any_cast<>(), ....
  • Java a des structures de contrôle intégrées (for(;;){}, while(){}, if(){}else{}, do{}while(), synchronized(){}, strictfp{}) Et ne vous laissez pas définir votre propre. Scala définit plutôt une syntaxe abstraite qui vous permet d'appeler des fonctions à l'aide d'une syntaxe de structure de contrôle pratique et de bibliothèques utilisez-la pour définir efficacement les nouvelles structures de contrôle (par exemple react{} dans la bibliothèque Acteurs).

En outre, vous pouvez consulter la fonctionnalité de syntaxe personnalisée de Mathematica dans le fichier package de notation . (Techniquement, c'est dans la famille Lisp, mais certaines caractéristiques d'extensibilité sont-elles réalisées différemment, ainsi que l'extensibilité habituelle LISP.)

10
Mechanical snail

Rebol Cela ressemble presque à ce que vous décrivez, mais un peu de côté.

Plutôt que de définir une syntaxe spécifique, tout dans Rebol est un appel de fonction - il n'y a pas de mots-clés. (Oui, vous pouvez redéfinir if et while si vous désirez vraiment). Par exemple, il s'agit d'une déclaration if:

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

if est une fonction qui prend 2 arguments: une condition et un bloc. Si la condition est vraie, le bloc est évalué. Cela ressemble à la plupart des langues, non? Eh bien, le bloc est une structure de données, elle n'est pas limitée au code - il s'agit d'un bloc de blocs, par exemple, et un exemple rapide de la flexibilité du "code est des données":

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"]

Tant que vous pouvez vous en tenir aux règles de la syntaxe, prolonger cette langue est, pour la plupart, ne serait plus rien de plus que de définir de nouvelles fonctions. Certains utilisateurs ont réussi des fonctionnalités de Rebol 3 dans Rebol 2, par exemple.

8
Izkata

Ruby a une syntaxe assez flexible, je pense que c'est un moyen de "étendre la langue dans la langue elle-même".

Un exemple est Rake . Il est écrit en rubis, c'est rubis, mais on dirait make .

Pour vérifier certaines possibilités, vous pouvez rechercher les mots-clés Ruby et MetaProgramming .

7
knut

Extension de la syntaxe La façon dont vous parlez vous permet de créer langages spécifiques de domaine. Donc, peut-être le moyen le plus utile de reformuler votre question est de savoir quelles autres langues ont un bon soutien aux langues spécifiques à domaines?

Ruby a une syntaxe très flexible et beaucoup de DSL ont prospéré là-bas, tel que le râteau. Groovy comprend beaucoup de cette bonté. Il inclut également AST Transforms, qui sont plus directement analogues aux macros LISP.

R, la langue de l'informatique statistique permet aux fonctions d'obtenir leurs arguments inévalués. Il utilise ceci pour créer un DSL pour spécifier la formule de régression. Par exemple:

y ~ a + b

signifie "Ajuster une ligne de la forme K0 + K1 * A + K2 * B sur les valeurs de y."

y ~ a * b

signifie "Ajuster une ligne de forme K0 + K1 * A + K2 * B + K3 * A * B sur les valeurs de y."

Etc.

7
Martin C. Martin

Converger est un autre langage de métaprogramming non liispy. Et, dans une certaine mesure, C++ se qualifie aussi.

Sans doute, Metaocaml est assez loin de LISP. Pour un style totalement différent d'extensibilité syntaxique, mais assez puissant, jetez un coup d'œil à CAMLP4 .

Nemerle est une autre langue extensible avec un métaprogramming de style LISP, bien qu'il soit plus proche de langues comme Scala.

Et Scala elle-même deviendra bientôt une telle langue aussi.

EDIT: J'ai oublié l'exemple le plus intéressant - MPS de JetBrains . Il n'est pas seulement très éloigné de quoi que ce soit de quelque chose de Lispish, c'est même un système de programmation non textuelle, avec un éditeur fonctionnant directement sur un AST = niveau.

Edit2: Pour répondre à une question mise à jour - il n'y a rien d'unique et exceptionnel dans les macros LISP. En théorie, toute langue peut fournir un tel mécanisme (je l'ai même fait avec la plaine C). Tout ce dont vous avez besoin est un accès à votre AST et une capacité d'exécution de code dans le temps de compilation. Une certaine réflexion pourrait aider (interroger sur les types, les définitions existantes, etc.).

7
SK-logic

Prolog permet de définir de nouveaux opérateurs traduits en termes composés du même nom. Par exemple, cela définit un opérateur has_cat et le définit comme prédicat de vérification si une liste contient atom cat:

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

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

Le xf signifie que has_cat est un opérateur de postfix; L'utilisation de fx en ferait un opérateur de préfixe et xfx en ferait un opérateur d'infixe, en prenant deux arguments. Chèque Ce lien Pour plus de détails sur la définition des opérateurs de Prolog.

6
Ambroz Bizjak

boo Vous permet de personnaliser lourdement la langue au moment de la compilation via des macros syntaxiques.

BOO a un "pipeline compilateur extensible". Cela signifie que le compilateur peut appeler votre code à faire AST Transformations à tout moment pendant le pipeline compilateur. Comme vous le savez, des choses comme les génériques de Java ou C # 's LINQ ne sont que des transformations de syntaxe au moment de la compilation. est tout à fait puissant.

Par rapport à LISP, le principal avantage est que cela fonctionne avec n'importe quel type de syntaxe. BOO utilise une syntaxe inspirée par Python, mais vous pouvez probablement écrire un compilateur extensible avec une syntaxe C ou Pascal. Et puisque la macro est évaluée au moment de la compilation, il n'y a pas de pénalité de performance.

Les inconvénients, comparés à LISP, sont:

  • Travailler avec un AST n'est pas aussi élégant que fonctionnant avec S-Expressions
  • Étant donné que la macro est invoquée à la compilation, il n'a pas accès aux données d'exécution.

Par exemple, c'est ainsi que vous pourriez implémenter une nouvelle structure de contrôle:

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

usage:

repeatLines 2, "foo", "bar"

qui est ensuite traduit, à la compilation, à quelque chose comme:

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

(Malheureusement, la documentation en ligne de BOO est toujours désespérément obsolète et ne couvre même pas de choses avancées comme celle-ci. La meilleure documentation pour la langue que je connaisse est ce livre: http://www.manning.com/rahien/ )

5
Dan

L'évaluation Mathematica est basée sur la correspondance et le remplacement des motifs. Cela vous permet de créer vos propres structures de contrôle, de modifier les structures de contrôle existantes ou de modifier la manière dont les expressions sont évaluées. Par exemple, vous pouvez mettre en œuvre une "logique floue" comme celle-ci (un peu simplifié):

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]

Cela remplace l'évaluation des opérateurs logiques prédéfinis &&, || ,! et la clause intégrée If.

Vous pouvez lire ces définitions telles que les définitions de la fonction, mais la signification réelle est la suivante: si une expression correspond au motif décrit sur le côté gauche, il est remplacé par l'expression du côté droit. Vous pouvez définir votre propre clause if-clause comme ceci:

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

SetAttributes[..., HoldRest] Indique à l'évaluateur qu'il devrait évaluer le premier argument avant la correspondance du modèle, mais détenir l'évaluation pour le reste jusqu'à ce que le motif ait été assorti et remplacé.

Ceci est largement utilisé dans les bibliothèques standard Mathematica pour par ex. Définir une fonction D qui prend une expression et évalue à son dérivé symbolique.

4
nikie

Metalua est une langue et un compilateur compatible avec Lua qui le fournit.

  • Compatibilité totale avec Lua 5.1 Sources et bytecode: nettoyage, sémantique élégante et syntaxe, puissance expressive incroyable, bonnes performances, portabilité proche universelle. Un système de macro complet, similaire au pouvoir de ce qui est de nouveau par des dialectes LISP ou de modèle HASKELL; Les programmes manipulés peuvent être vus
    [.____] comme code source, en tant que syntaxe abstraite, ou en tant que mélange arbitraire
    Celui-ci correspond à votre tâche mieux.
  • Un analyseur extensible dynamiquement, ce qui vous permet de prendre en charge vos macros avec une syntaxe qui se fond bien avec le reste de la langue.

  • Un ensemble d'extensions de langues, toutes implémentées comme des macros de métalua ordinaires.

Différences avec Lisp:

  • Ne percez pas les développeurs avec des macros quand ils ne l'écrivent pas: La syntaxe et la sémantique de la langue devraient être mieux adaptées à ces 95% du temps où nous n'écrivons pas les macros.
  • Encouragez les développeurs à suivre les conventions de la langue: non seulement avec les "meilleures pratiques", personne n'écoute, mais en offrant une API qui facilite l'écriture des choses de la voie Metalua. La lisibilité par d'autres développeurs est plus importante et plus difficile à réaliser que la lisibilité des compilateurs et, pour cela, avoir un ensemble commun de conventions respectées contribue beaucoup.
  • Pourtant, fournissez tout le pouvoir disposé à gérer. Ni Lua ni Metalua ne sont en bondage et discipline obligatoires, donc si vous savez ce que vous faites, la langue ne se mettra pas dans votre chemin.
  • Faites-le évident quand quelque chose d'intéressant arrive: toutes les méta-opérations se produisent entre + {...} et - {...} et sortent visuellement du code normal.

Un exemple d'application est la mise en œuvre de la correspondance de modèle de type ML.

Voir aussi: http://lua-users.org/wiki/metalua

3
Clement J.

Si vous recherchez des langues qui sont extensibles, vous devez jeter un coup d'œil à Smalltalk.

Dans SmallTalk, le Seule le seul moyen au programme consiste à prolonger la langue. Il n'y a pas de différence entre l'IDE, les bibliothèques ou la langue elle-même. Ils sont tous aussi enroulé que Smalltalk est souvent appelé environnement plutôt que comme langue.

Vous n'écrivez pas aux applications autonomes dans Smalltalk, vous étendez la langue-environnement.

Vérifiez http://www.world.st/ pour une poignée de ressources et d'informations.

J'aimerais recommander Pharo comme le dialecte d'entrée dans le monde de Smalltalk: http://pharo-project.org

J'espère que cela a aidé!

2
Bernat Romagosa

Il existe des outils qui permettent de créer des langues personnalisées sans écrire un compilateur entier à partir de zéro. Par exemple, il existe spoofax , qui est un outil de transformation de code: vous mettez dans des règles de grammaire d'entrée et de transformation (écrites de manière déclarée de très haut niveau), puis vous pouvez générer Java code source (ou autre langue, si vous vous souciez suffisamment) d'une langue personnalisée conçue par vous.

Donc, il serait possible de prendre la grammaire de la langue x, de définir la grammaire de langue x '(x avec vos extensions personnalisées) et de transformation x' → x et spoofax générera un compilateur x '→ x.

Actuellement, si je comprends bien, le meilleur soutien est pour Java, avec le développement C # développé (ou j'ai donc entendu). Cette technique pourrait être appliquée à n'importe quelle langue avec la grammaire statique (donc, par ex. probablement pas perl ) cependant.

1
liori

avant est une autre langue très extensible. De nombreuses implémentations sont constituées d'un petit noyau écrit dans Assembleur ou C, puis le reste de la langue est écrit en avant-même.

Il existe également plusieurs langues de la pile inspirées par et partagent cette fonctionnalité, telle que facteur .

1
Dave Kirby

Funge-98

La fonction d'empreinte digitale de Funge-98 permet de pouvoir être effectuée pour permettre une restructuration complète de la syntaxe et de la sémantique de la langue. Mais uniquement si la mise en œuvre fournit un mécanisme d'empreintes digitales qui permettait à l'utilisateur de modifier programmablement le language (il est théoriquement possible de mettre en œuvre dans la syntaxe et la sémantique de Funge-98). Si tel est le cas, on pourrait littéralement rendre le reste du fichier (ou quelles que soient certaines parties du fichier) agissent comme C++ ou LISP (ou quoi qu'il veuille).

http://quadium.net/funge/spec98.html#fingerprints

0
Thomas Eding