web-dev-qa-db-fra.com

LET versus LET * en Common Lisp

Je comprends la différence entre LET et LET * (liaison parallèle versus séquentielle) et, d’un point de vue théorique, cela prend tout son sens. Mais y a-t-il des cas où vous avez déjà eu besoin de LET? Dans tout le code LISP que j'ai consulté récemment, vous pouvez remplacer chaque LET par LET * sans changement.

Edit: OK, je comprends pourquoi un type a inventé LET *, vraisemblablement sous forme de macro, il y a bien longtemps. Ma question est, étant donné que LET * existe, y a-t-il une raison pour que LET reste ici? Avez-vous écrit un code LISP réel dans lequel un LET * ne fonctionnerait pas aussi bien qu'un LET simple?

Je n'achète pas l'argument d'efficacité. Premièrement, reconnaître les cas où LET * peut être compilé en quelque chose d'aussi efficace que LET ne semble tout simplement pas si difficile. Deuxièmement, il y a beaucoup d'éléments de la spécification CL qui ne semblent tout simplement pas avoir été conçus pour l'efficacité. (Quand avez-vous vu pour la dernière fois une boucle avec des déclarations de type? Celles-ci sont si difficiles à comprendre que je ne les ai jamais vues utilisées.) Avant les temps-repères de Dick Gabriel, CL était carrément lent.

Cela ressemble à un autre cas de compatibilité ascendante: à bon escient, personne ne voulait risquer de casser quelque chose d'aussi fondamental que LET. C'était mon intuition, mais il est réconfortant d'entendre que personne ne me manque d'un cas aussi simple que stupide où LET a rendu ridicule tout un tas de choses plus faciles que LET *.

79
Ken

LET n'est pas une vraie primitive dans un langage de programmation fonctionnel, car il peut être remplacé par LAMBDA. Comme ça:

(let ((a1 b1) (a2 b2) ... (an bn))
  (some-code a1 a2 ... an))

est similaire à

((lambda (a1 a2 ... an)
   (some-code a1 a2 ... an))
 b1 b2 ... bn)

Mais

(let* ((a1 b1) (a2 b2) ... (an bn))
  (some-code a1 a2 ... an))

est similaire à

((lambda (a1)
    ((lambda (a2)
       ...
       ((lambda (an)
          (some-code a1 a2 ... an))
        bn))
      b2))
   b1)

Vous pouvez imaginer quelle est la chose la plus simple. LET et non LET*.

LET facilite la compréhension du code. On voit un tas de liaisons et on peut lire chaque liaison individuellement sans avoir besoin de comprendre le flux «d'effets» de haut en bas/de gauche à droite. Utiliser LET* signale au programmeur (celui qui lit le code) que les liaisons ne sont pas indépendantes, mais qu'il existe une sorte de flux descendant - ce qui complique les choses.

Common LISP a pour règle que les valeurs des liaisons dans LET sont calculées de gauche à droite. Comment les valeurs d'un appel de fonction sont évaluées - de gauche à droite. Ainsi, LET est l’énoncé conceptuellement plus simple et doit être utilisé par défaut.

Types dans LOOP? Sont utilisés assez souvent. Certaines formes primitives de déclaration de type sont faciles à mémoriser. Exemple:

(LOOP FOR i FIXNUM BELOW (TRUNCATE n 2) do (something i))

Ci-dessus, la variable i est définie comme étant une fixnum.

Richard P. Gabriel a publié son livre sur les repères du LISP en 1985. À cette époque, ces repères étaient également utilisés avec des Lisps non CL. Le LISP commun lui-même était tout neuf en 1985 - le livre CLtL1 qui décrivait le langage venait de paraître en 1984. Rien d'étonnant à ce que les implémentations n'aient pas été très optimisées à l'époque. Les optimisations mises en œuvre étaient fondamentalement les mêmes (ou moins) que les implémentations précédentes (comme MacLisp). 

Mais pour LET par rapport à LET*, la principale différence est que le code utilisant LET est plus facile à comprendre pour les humains, car les clauses de liaison sont indépendantes les unes des autres - d’autant plus que c’est mauvais style de tirer parti de l’évaluation de gauche à droite comme effet secondaire).

79
Rainer Joswig

Vous n'avez pas besoin LET, mais vous l'utilisez normalement voulez.

LET suggère que vous ne fassiez que la reliure parallèle standard sans que rien ne soit délicat. LET * induit des restrictions sur le compilateur et suggère à l'utilisateur qu'il existe une raison pour laquelle des liaisons séquentielles sont nécessaires. En termes de style, LET est préférable lorsque vous n'avez pas besoin des restrictions supplémentaires imposées par LET *.

Il peut être plus efficace d’utiliser LET que LET * (en fonction du compilateur, de l’optimiseur, etc.):

  • les liaisons parallèles peuvent être exécutées en parallèle (mais je ne sais pas si un système LISP le fait réellement et les formulaires init doivent toujours être exécutés séquentiellement)
  • les liaisons parallèles créent un nouvel environnement unique (portée) pour toutes les liaisons. Les liaisons séquentielles créent un nouvel environnement imbriqué pour chaque liaison. Les liaisons parallèles utilisent moins de mémoire et ont une recherche de variable plus rapide.

(Les points ci-dessus s'appliquent à Scheme, un autre dialecte LISP. Clisp peut différer.)

34
Mr Fooz

Je viens portant des exemples artificiels. Comparez le résultat de ceci:

(print (let ((c 1))
         (let ((c 2)
               (a (+ c 1)))
           a)))

avec le résultat de l'exécution de ceci:

(print (let ((c 1))
         (let* ((c 2)
                (a (+ c 1)))
           a)))
22
Logan Capaldo

Dans LISP, on souhaite souvent utiliser les constructions les plus faibles possibles. Certains guides de style vous diront d'utiliser = plutôt que eql lorsque vous savez que les éléments comparés sont numériques, par exemple. L'idée est souvent de spécifier ce que vous voulez dire plutôt que de programmer l'ordinateur de manière efficace.

Cependant, il peut y avoir des gains d’efficacité réels en ne disant que ce que vous voulez dire, et en n’utilisant pas des concepts plus forts. Si vous avez des initialisations avec LET, elles peuvent être exécutées en parallèle, tandis que les initialisations LET* doivent être exécutées de manière séquentielle. Je ne sais pas si des implémentations le feront réellement, mais certaines pourraient très bien dans le futur.

10
David Thornley

La principale différence dans la liste commune entre LET et LET * est que les symboles de LET sont liés en parallèle et de LET *, de manière séquentielle. Utiliser LET n'autorise pas l'exécution parallèle des formes-init, ni ne modifie l'ordre des formes-init. La raison en est que Common LISP permet aux fonctions d’avoir des effets secondaires. Par conséquent, l'ordre d'évaluation est important et est toujours de gauche à droite dans un formulaire. Ainsi, dans LET, les formes init sont évaluées d’abord, de gauche à droite, puis les liaisons sont créées, de gauche à droite en parallèle. Dans LET *, la forme-init est évaluée, puis liée au symbole dans l'ordre, de gauche à droite.

CLHS: Opérateur spécial LET, LET *

9
tmh

J'écrivais récemment une fonction de deux arguments, où l'algorithme est exprimé le plus clairement si nous savons quel argument est le plus grand.

(defun foo (a b)
  (let ((a (max a b))
        (b (min a b)))
    ; here we know b is not larger
    ...)
  ; we can use the original identities of a and b here
  ; (perhaps to determine the order of the results)
  ...)

Si b était plus grand, si nous avions utilisé let*, nous aurions accidentellement défini a et b sur la même valeur.

9
Samuel Edwin Ward

je vais un peu plus loin et utilise bind qui unifie let, let*, multiple-value-bind, destructuring-bind etc., et il est même extensible.

en général, j'aime bien utiliser la "construction la plus faible", mais pas avec let & friends car ils ne font que donner du bruit au code (avertissement de subjectivité! inutile de tenter de me convaincre du contraire ...)

8
Attila Lendvai
(let ((list (cdr list))
      (pivot (car list)))
  ;quicksort
 )

Bien sûr, cela fonctionnerait:

(let* ((rest (cdr list))
       (pivot (car list)))
  ;quicksort
 )

Et ça:

(let* ((pivot (car list))
       (list (cdr list)))
  ;quicksort
 )

Mais c'est la pensée qui compte.

4
Zorf

Vraisemblablement, en utilisant let, le compilateur a plus de flexibilité pour réorganiser le code, peut-être pour améliorer la vitesse ou l'espace.

Stylistiquement, l’utilisation de liaisons parallèles montre l’intention de regrouper les liaisons; cela est parfois utilisé pour conserver des liaisons dynamiques:

(let ((*PRINT-LEVEL* *PRINT-LEVEL*)
      (*PRINT-LENGTH* *PRINT-LENGTH*))
  (call-functions that muck with the above dynamic variables)) 
4
Doug Currie

Le PO demande "jamais vraiment eu besoin de LET"?

Lors de la création de Common LISP, il y avait une charge de bateaux du code LISP existant dans divers dialectes. Le concept accepté par les concepteurs de Common LISP était de créer un dialecte du LISP qui fournirait un terrain d’entente. Ils avaient "besoin" de rendre facile et attrayant le portage du code existant dans Common LISP. L'abandon de LET ou LET * de la langue aurait pu servir d'autres vertus, mais cet objectif clé aurait été ignoré.

J'utilise LET de préférence à LET * car cela indique au lecteur quelque chose sur le déroulement du flux de données. Dans mon code, au moins, si vous voyez un LET *, vous savez que les valeurs liées tôt seront utilisées dans une liaison ultérieure. Est-ce que j'ai "besoin" de faire ça, non; mais je pense que c'est utile. Cela dit, j'ai rarement lu le code LET * par défaut et l'apparition de LET signale que l'auteur le voulait vraiment. C'est à dire. par exemple pour échanger le sens de deux vars.

(let ((good bad)
     (bad good)
...)

Il existe un scénario discutable qui aborde le «besoin réel». Il se pose avec des macros. Cette macro:

(defmacro M1 (a b c)
 `(let ((a ,a)
        (b ,b)
        (c ,c))
    (f a b c)))

fonctionne mieux que

(defmacro M2 (a b c)
  `(let* ((a ,a)
          (b ,b)
          (c ,c))
    (f a b c)))

depuis (M2 c b a) ne va pas marcher. Mais ces macros sont plutôt négligées pour diverses raisons; de sorte que sape l'argument du «besoin réel».

2
Ben Hyde

En plus de Rainer Joswig answer, et d'un point de vue puriste ou théorique. Let & Let * représente deux paradigmes de programmation; fonctionnel et séquentiel respectivement.

Pour ce qui est de pourquoi devrais-je continuer à utiliser Let * au lieu de Let, eh bien, vous prenez le plaisir de revenir à la maison et de penser en langage purement fonctionnel, par opposition au langage séquentiel dans lequel je passe la majeure partie de ma journée à travailler :)

1
emb

L'opérateur let introduit un environnement unique pour toutes les liaisons qu'il spécifie. let*, du moins conceptuellement (et si nous ignorons les déclarations pour un moment) introduit plusieurs environnements: 

C'est-à-dire:

(let* (a b c) ...)

est comme:

(let (a) (let (b) (let (c) ...)))

Donc, dans un sens, let est plus primitif, alors que let* est un sucre syntaxique pour écrire une cascade de let- s.

Mais peu importe. (Et je donnerai plus tard la justification ci-dessous pour expliquer pourquoi nous devrions "ne jamais en tenir compte"). Le fait est qu'il y a deux opérateurs, et dans "99%" du code, peu importe celui que vous utilisez. La raison pour préférer let au let* est simplement qu’elle n’a pas le * en suspens à la fin.

Chaque fois que vous avez deux opérateurs et que l’un a un * suspendu à son nom, utilisez celui qui n’a pas le * si cela fonctionne dans cette situation, pour que votre code soit moins laid.

C'est tout ce qu'il y a à faire.

Cela étant dit, je soupçonne que si let et let* échangeaient leurs significations, il serait probablement plus rare d'utiliser let* que maintenant. Le comportement de liaison série ne gênera pas la plupart des codes qui n'en ont pas besoin: il faudrait rarement utiliser le let* pour demander un comportement parallèle (et de telles situations pourraient également être corrigées en renommant des variables pour éviter l'observation).

Maintenant pour cette discussion promise. Bien que let* introduit conceptuellement plusieurs environnements, il est très facile de compiler la construction let* de manière à générer un seul environnement. Donc même si (au moins si nous ignorons les déclarations ANSI CL), il existe une égalité algébrique selon laquelle un seul let* correspond à plusieurs let- s imbriqués, ce qui donne à let un aspect plus primitif, il n’ya aucune raison de développer let* dans let et même une mauvaise idée.

Une dernière chose: notez que la variable lambda de Common LISP utilise en réalité une sémantique de type let*-! Exemple:

(lambda (x &optional (y x) (z (+1 y)) ...)

ici, le x init-form pour y accède au paramètre précédent x et, de la même manière, (+1 y) fait référence à la version précédente y optionnelle. Dans cette zone de la langue, une préférence claire pour la visibilité séquentielle dans la liaison est visible. Il serait moins utile que les formulaires de la variable lambda ne puissent pas voir les paramètres. un paramètre facultatif ne pouvait pas être défini par défaut succinctement sur la valeur des paramètres précédents.

1
Kaz

Avec Permet d'utiliser la liaison parallèle,

(setq my-pi 3.1415)

(let ((my-pi 3) (old-pi my-pi))
     (list my-pi old-pi))
=> (3 3.1415)

Et avec Let * serial binding,

(setq my-pi 3.1415)

(let* ((my-pi 3) (old-pi my-pi))
     (list my-pi old-pi))
=> (3 3)
1
Floydan