67 votes

Un exemple pratique de la flexibilité de Lisp ?

Quelqu'un essaie de me vendre Lisp, comme un langage super puissant qui peut faire tout ce qui a été fait, et même plus.

Y a-t-il un pratique exemple de code de la puissance de Lisp ?
(De préférence à côté d'une logique équivalente codée dans un langage régulier).

77voto

Mikael Jansson Points 3234

J'aime les macros.

Voici un code qui permet d'extraire les attributs des personnes à partir de LDAP. Il se trouve que j'avais ce code qui traînait et j'ai pensé qu'il pourrait être utile à d'autres.

Certaines personnes sont confuses quant à une supposée pénalité d'exécution des macros, j'ai donc ajouté une tentative de clarification à la fin.

Au début, il y avait la duplication

(defun ldap-users ()
  (let ((people (make-hash-table :test 'equal)))
    (ldap:dosearch (ent (ldap:search *ldap* "(&(telephonenumber=*) (cn=*))"))
                   (let ((mail  (car (ldap:attr-value ent 'mail)))
                         (uid   (car (ldap:attr-value ent 'uid)))
                         (name  (car (ldap:attr-value ent 'cn)))
                         (phonenumber (car (ldap:attr-value ent 'telephonenumber))))
                      (setf (gethash uid people)
                            (list mail name phonenumber))))
    people))

Vous pouvez considérer un "let binding" comme une variable locale, qui disparaît en dehors de la forme LET. Remarquez la forme des liaisons -- elles sont très similaires, ne différant que par l'attribut de l'entité LDAP et le nom ("variable locale") auquel lier la valeur. Utile, mais un peu verbeux et contenant des doublons.

La quête de la beauté

Ne serait-il pas agréable de ne pas avoir à faire toutes ces répétitions ? Un idiome commun est la macro WITH-..., qui lie les valeurs en fonction d'une expression dont vous pouvez extraire les valeurs. Introduisons notre propre macro qui fonctionne de cette manière, WITH-LDAP-ATTRS, et remplaçons-la dans notre code original.

(defun ldap-users ()
  (let ((people (make-hash-table :test 'equal))) ; equal so strings compare equal!
    (ldap:dosearch (ent (ldap:search *ldap* "(&(telephonenumber=*) (cn=*))"))
                   (with-ldap-attrs (mail uid name phonenumber) ent
                       (setf (gethash uid people)
                             (list mail name phonenumber))))
    people))

Avez-vous vu comment un groupe de lignes a soudainement disparu, et a été remplacé par une seule ligne ? Comment faire cela ? En utilisant des macros, bien sûr - du code qui écrit du code ! Les macros en Lisp sont un animal totalement différent de celles que vous pouvez trouver en C/C++ grâce à l'utilisation du pré-processeur : ici, vous pouvez exécuter réel Le code Lisp (pas le #define fluff in cpp) qui génère du code Lisp, avant que l'autre code ne soit compilé. Les macros peuvent utiliser n'importe quel code Lisp réel, c'est-à-dire des fonctions ordinaires. Essentiellement aucune limite.

Se débarrasser du laid

Voyons donc comment cela a été fait. Pour remplacer un attribut, nous définissons une fonction.

(defun ldap-attr (entity attr)
  `(,attr (car (ldap:attr-value ,entity ',attr))))

La syntaxe de la citation inverse est un peu compliquée, mais son utilité est simple. Lorsque vous appelez LDAP-ATTRS, il vous enverra une liste contenant les éléments suivants valeur de attr (c'est la virgule), suivi de car ("premier élément de la liste" (paire de cons, en fait), et il existe en fait une fonction appelée first que vous pouvez aussi utiliser), qui reçoit la première valeur de la liste retournée par ldap:attr-value . Parce que ce n'est pas le code que nous voulons exécuter quand nous compilons le code (obtenir les valeurs d'attribut est ce que nous voulons faire quand nous ejecute le programme), nous n'ajoutons pas de virgule avant l'appel.

Bref. Passons au reste de la macro.

(defmacro with-ldap-attrs (attrs ent &rest body)
  `(let ,(loop for attr in attrs
         collecting `,(ldap-attr ent attr))
     ,@body))

El ,@ La syntaxe consiste à mettre le contenu d'une liste quelque part, au lieu de la liste elle-même.

Résultat

Vous pouvez facilement vérifier que cela vous donnera la bonne chose. Les macros sont souvent écrites de cette manière : vous commencez par le code que vous voulez simplifier (la sortie), ce que vous voulez écrire à la place (l'entrée), puis vous commencez à modeler la macro jusqu'à ce que votre entrée donne la sortie correcte. La fonction macroexpand-1 vous dira si votre macro est correcte :

(macroexpand-1 '(with-ldap-attrs (mail phonenumber) ent
                  (format t "~a with ~a" mail phonenumber)))

évalue à

(let ((mail (car (trivial-ldap:attr-value ent 'mail)))
      (phonenumber (car (trivial-ldap:attr-value ent 'phonenumber))))
  (format t "~a with ~a" mail phonenumber))

Si vous comparez les liaisons LET de la macro développée avec le code du début, vous constaterez qu'il s'agit de la même forme !

Compilation et exécution : Macros et fonctions

Une macro est un code qui est exécuté à temps de compilation avec l'avantage supplémentaire qu'ils peuvent appeler n'importe qui. ordinaire ou macro comme bon leur semble ! Ce n'est pas beaucoup plus qu'un filtre fantaisiste, prenant quelques arguments, appliquant quelques transformations et fournissant ensuite au compilateur les s-exps résultantes.

En gros, il vous permet d'écrire votre code en verbes que l'on peut trouver dans le domaine du problème, au lieu de primitives de bas niveau du langage ! A titre d'exemple, considérez ce qui suit (si when n'était pas déjà intégré) : :

(defmacro my-when (test &rest body)
  `(if ,test 
     (progn ,@body)))

if est une primitive intégrée qui vous permettra seulement d'exécuter un dans les branches, et si vous voulez en avoir plus d'une, eh bien, vous devez utiliser le formulaire progn : :

;; one form
(if (numberp 1)
  (print "yay, a number"))

;; two forms
(if (numberp 1)
  (progn
    (assert-world-is-sane t)
    (print "phew!"))))

Avec notre nouvel ami, my-when, nous pourrions à la fois a) utiliser le verbe le plus approprié si nous n'avons pas de fausse branche, et b) ajouter un opérateur de séquencement implicite, c'est-à-dire progn : :

(my-when (numberp 1)
  (assert-world-is-sane t)
  (print "phew!"))

Le code compilé ne contiendra jamais my-when Cependant, parce que lors de la première passe, toutes les macros sont développées et il y a donc pas de pénalité de temps d'exécution impliqué !: :

Lisp> (macroexpand-1 '(my-when (numberp 1)
                        (print "yay!")))

(if (numberp 1)
  (progn (print "yay!")))

Notez que macroexpand-1 ne fait qu'un seul niveau d'extensions ; il est possible (très probable, en fait !) que les extensions continuent plus bas. Cependant, vous finirez par atteindre les détails d'implémentation spécifiques au compilateur qui ne sont souvent pas très intéressants. Mais en continuant à développer le résultat, vous obtiendrez soit plus de détails, soit simplement votre s-exp d'entrée.

J'espère que cela clarifie les choses. Les macros sont un outil puissant, et l'une des fonctionnalités de Lisp que j'apprécie.

18voto

Jason Dagit Points 5998

Le meilleur exemple auquel je pense et qui est largement disponible est le livre de Paul Graham, Sur Lisp . Le PDF complet peut être téléchargé à partir du lien que je viens de donner. Vous pouvez également essayer Pratique du Common Lisp (également disponible en intégralité sur le web).

J'ai beaucoup d'exemples peu pratiques. J'ai écrit une fois un programme en 40 lignes de lisp qui pouvait s'analyser lui-même, traiter sa source comme une liste lisp, faire une traversée d'arbre de la liste et construire une expression qui évaluait WALDO si l'identifiant waldo existait dans la source ou évaluait nil si waldo n'était pas présent. L'expression retournée a été construite en ajoutant des appels à car/cdr à la source originale qui a été analysée. Je n'ai aucune idée de comment faire cela dans d'autres langages en 40 lignes de code. Peut-être que perl peut le faire en encore moins de lignes.

14voto

Matthias Benkard Points 11264

Vous trouverez peut-être cet article utile : http://www.defmacro.org/ramblings/lisp.html

Cela dit, il est très, très difficile de donner des exemples courts et pratiques de la puissance de Lisp, car il ne brille vraiment que dans du code non trivial. Lorsque votre projet atteindra une certaine taille, vous apprécierez les facilités d'abstraction de Lisp et serez heureux de les avoir utilisées. Des échantillons de code raisonnablement courts, d'un autre côté, ne vous donneront jamais une démonstration satisfaisante de ce qui fait la grandeur de Lisp, car les abréviations prédéfinies des autres langages sembleront plus attrayantes dans les petits exemples que la flexibilité de Lisp dans la gestion des abstractions spécifiques au domaine.

13voto

Will Hartung Points 57465

En fait, un bon exemple pratique est la macro LOOP de Lisp.

http://www.ai.sri.com/pkarp/loop.html

La macro LOOP est simplement cela : une macro Lisp. Pourtant, elle définit fondamentalement un mini DSL (Domain Specific Language) de bouclage.

Lorsque vous parcourez ce petit tutoriel, vous pouvez constater (même en tant que novice) qu'il est difficile de savoir quelle partie du code fait partie de la macro Loop, et quelle partie est du Lisp "normal".

Et c'est l'un des éléments clés de l'expressivité de Lisps, que le nouveau code ne puisse pas vraiment être distingué du système.

Alors qu'en Java, par exemple, vous ne pouvez pas (d'un seul coup d'œil) savoir quelle partie d'un programme provient de la bibliothèque Java standard ou de votre propre code, ou même d'une bibliothèque tierce, vous savez quelle partie du code est le langage Java plutôt que de simples appels de méthodes sur des classes. Certes, c'est TOUT le "langage Java", mais en tant que programmeur, vous êtes limité à l'expression de votre application comme une combinaison de classes et de méthodes (et maintenant, d'annotations). Alors qu'en Lisp, tout est littéralement à portée de main.

Considérez l'interface Common SQL pour connecter Common Lisp à SQL. Ici, http://clsql.b9.com/manual/loop-tuples.html Ils montrent comment la macro CL Loop est étendue pour faire de la liaison SQL un "citoyen de première classe".

Vous pouvez également observer des constructions telles que "[select [first-name] [last-name] :from [employee] :order-by [last-name]]". Cela fait partie du paquet CL-SQL et est implémenté comme une "macro de lecture".

Vous voyez, en Lisp, non seulement vous pouvez faire des macros pour créer de nouvelles constructions, comme des structures de données, des structures de contrôle, etc. Mais vous pouvez même changer la syntaxe du langage à travers une macro de lecture. Ici, ils utilisent une macro de lecture (dans ce cas, le symbole '[') pour passer en mode SQL et faire fonctionner le SQL comme du SQL intégré, plutôt que comme des chaînes brutes comme dans beaucoup d'autres langages.

En tant que développeurs d'applications, notre tâche consiste à convertir nos processus et nos constructions en une forme que le processeur peut comprendre. Cela signifie que nous devons inévitablement "parler bas" au langage informatique, puisqu'il ne nous "comprend" pas.

Common Lisp est l'un des rares environnements où nous pouvons non seulement construire notre application du haut vers le bas, mais où nous pouvons soulever le langage et l'environnement pour nous rencontrer à mi-chemin. Nous pouvons coder aux deux extrémités.

Attention, aussi élégant que cela puisse être, ce n'est pas une panacée. Il existe évidemment d'autres facteurs qui influencent le choix de la langue et de l'environnement. Mais cela vaut certainement la peine de l'apprendre et de jouer avec. Je pense que l'apprentissage de Lisp est un excellent moyen de faire progresser votre programmation, même dans d'autres langages.

13voto

Mikael Jansson Points 3234

J'aime CLOS et les multiméthodes.

La plupart des langages de programmation orientés objet, sinon tous, possèdent les notions de base de classes et de méthodes. Le snippet suivant en Python définit les classes PeelingTool et Vegetable (quelque chose de similaire au modèle Visitor) :

class PeelingTool:
    """I'm used to peel things. Mostly fruit, but anything peelable goes."""
    def peel(self, veggie):
        veggie.get_peeled(self)

class Veggie:
    """I'm a defenseless Veggie. I obey the get_peeled protocol
    used by the PeelingTool"""
    def get_peeled(self, tool):
        pass

class FingerTool(PeelingTool):
  ...

class KnifeTool(PeelingTool):
  ...

class Banana(Veggie):
    def get_peeled(self, tool):
        if type(tool) == FingerTool:
            self.hold_and_peel(tool)
        elif type(tool) == KnifeTool:
            self.cut_in_half(tool)

Vous mettez le peel dans l'outil d'épluchage et que la banane l'accepte. Mais cette méthode doit appartenir à la classe PeelingTool, elle ne peut donc être utilisée que si vous disposez d'une instance de la classe PeelingTool.

La version du système d'objets Common Lisp :

(defclass peeling-tool () ())
(defclass knife-tool (peeling-tool) ())
(defclass finger-tool (peeling-tool) ())

(defclass veggie () ())
(defclass banana (veggie) ())

(defgeneric peel (veggie tool)
  (:documentation "I peel veggies, or actually anything that wants to be peeled"))

;; It might be possible to peel any object using any tool,
;; but I have no idea how. Left as an exercise for the reader
(defmethod peel (veggie tool)
   ...)

;; Bananas are easy to peel with our fingers!
(defmethod peel ((veggie banana) (tool finger-tool))
  (with-hands (left-hand right-hand) *me*
    (hold-object left-hand banana)
    (peel-with-fingers right-hand tool banana)))

;; Slightly different using a knife
(defmethod peel ((veggie banana) (tool knife-tool))
  (with-hands (left-hand right-hand) *me*
    (hold-object left-hand banana)
    (cut-in-half tool banana)))

Tout peut être écrit dans n'importe quel langage complet de Turing ; la différence entre les langages est le nombre d'obstacles à franchir pour obtenir un résultat équivalent.

Un langage puissant comme Common Lisp, avec des fonctionnalités telles que les macros et le CLOS, vous permet d'obtenir des résultats rapidement et facilement, sans avoir à franchir tant d'obstacles que vous vous contenterez d'une solution médiocre ou que vous deviendrez un kangourou.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X