Yliur <
yl...@free.fr> writes:
> Le Tue, 23 Apr 2013 19:29:36 +0200
> "Pascal J. Bourguignon" <
p...@informatimago.com> a écrit :
>
>> > J'aurai sans doute quelques questions après avoir essayé moi-même
>> > d'organiser mes fichiers et paquetages, mais une première
>> > difficulté agaçante : defpackage ne permet pas de redéfinir le
>> > paquetage (enfin d'après la spécification ça dépend de
>> > l'implémentation si j'ai bien compris).
>>
>> Oui, c'est la "théorie", en effet.
>>
>> (voir com.informatimago.common-lisp.lisp-reader.package pour le source
>> d'une macro defpackage).
>
> Je ne l'ai pas trouvé, mais j'ai regardé ce qu'affichait macroexpand-1.
Désolé, j'ai oublié de mettre le lien:
https://gitorious.org/com-informatimago/com-informatimago/trees/master/common-lisp/lisp-reader
>> > Donc comment faire pour ne pas redémarrer tout l'interpréteur
>> > Lisp et recharger tout le projet à chaque fois que j'ajoute une
>> > fonction dans un paquetage existant ? C'est assez lourd.
>>
>> En pratique, en général ça marche. C'est à dire que le comportement
>> spécifique à l'implémentation se concentre plutôt sur les restarts qui
>> sont disponibles quand un problème se pose.
>
> J'avais pourtant bien l'impression d'avoir eu le problème hier. Bon,
> ben j'ai dû me mélanger quelque part, en faisant une nouvelle
> modification sur le code tout semble se passer correctement finalement.
Oui, c'est possible dans certains cas.
Mais le cas normal, c'est d'ajouter ou de renommer un symbole dans la
liste d'exportation. Ça ne pose pas de problème sur la plupart des
implémentations.
> Je vais enchaîner sur quelques autres questions un peu liées, apparues
> lors de la mise en place du projet. Les remarques diverses sur
> tout sont les bienvenues, je cherche encore...
>
>
> * Comme je n'ai pas envie de définir un paquetage pour chaque
> micro-fichier que je vais créer ni d'avoir des fichiers source
> immenses, j'ai parfois plusieurs fichiers sources pour un même
> paquetage.
Oui, c'est la bonne solution.
> Je crée un fichier paquetage.lisp par paquetage, qui
> contient defpackage et les expressions (load "...") permettant de
> charger les différents fichiers sources.
Non, il vaut mieux éviter les load et require dans les fichiers
sources. Tu peux les placer dans un fichier loader.lisp, ou mieux,
utiliser asdf (et donc quicklisp), en créant un fichier .asd définissant
les dépendences entre les fichiers sources (équivalent à un Makefile,
mais en mieux).
> Ceux-ci commencent par une ligne in-package, pour être sûr que les
> fonctions soient bien définies dans le bon paquetage.
Oui, en général, de nos jours on fait ça.
On pourrait aussi ne pas mettre de forme in-package dans les fichiers
sources, ce qui permet de les charger dans des packages différents (par
exemple dans un fichier loader.lisp on peut avoir des formes in-package
et load).
Mais ça ne serait pas trés pratique avec asdf/quicklisp, et ce n'est pas
souvent utile de pouvoir charger les sources dans des packages
différents, car il y a une forte homogeneisation des implémentations
lisp de nos jour: c'est tout du Common Lisp. Mais il y a vingt ans, ça
pouvait être intéressant de ne pas mettre de forme in-package.
Mais ça peut servir de ne pas mettre in-package. Par exemple, on
pourrait définir la syntaxe HTML déclarativement avec des macros
defelement, defattribute et defentity. Si on les met toutes dans un
fichier sans in-package, on peut définir ces macros dans un paquetage
pour générer des opérateur pour générer du HTML (en chargeant le fichier
contenant les définitions), et on peut les définir dans un autre
paquetage pour analyser du HTML et générer un arbre syntaxique (une
sexp), en chargeant le même fichier contenant les définitions. Plus
éventuellement, définir ces macros dans emacs pour implémenter de
l'édition structurée de documents HTML, et chargeant ce fichier dans
emacs!
> Il sera toujours temps de redécouper
> certains paquetages plus tard, en conservant l'ancien paquetage qui
> ne ferait que réexporter les symboles des sous-paquetages.
Absolument.
On peut faire correspondre les paquetages à la notion de module.
Un module étant un ensemble fonctionnel qui peuvent être développé
indépendament par un programmeur. (Si deux programmeurs dans une équipe
travaille sur le même module, c'est un signe qu'il faut peut être couper
ce module en sous-modules, et donc introduire des paquetages
supplémentaires).
> * J'ai des problèmes avec les chemin des fichiers sources : pour
> l'instant dans chaque fichier de paquetage j'ai écrit des choses
> comme (load "chemin/vers/le/paquetage/source.lisp"), ce qui n'est pas
> très commode parce qu'il faut écrire le chemin complet du fichier
> source à chaque fois. Et je ne peux lancer l'implémentation de
> Lisp que depuis la racine des sources, sinon les chemins ne sont
> pas bons. Là je me demande s'il n'y a pas moyen de faire plus souple
> (plus local/relatif), sans nécessairement me lancer tout de suite
> dans ASDF.
ASDF n'est pas compliqué. Donc mon conseil, c'est de commencer à
l'utiliser dès qu'on a plus de deux ou trois fichiers sources.
Mais sinon, tu peux t'amuser (c'est éducatif) à écrire à moitié, une
moitié d'ASDF. Vois par exemple *load-pathname* ou *load-truename*
ainsi que *compile-file-pathname* et *compile-file-truename*.
Avec les fonctions de manipulation des pathnames, tu peux facilement
définir un mini DSL pour charger les fichiers de ton projet simplement.
> * J'ai utilisé l'option "use" de defpackage, mais j'ai hésité à
> utiliser plutôt use-package dans chacun des fichiers sources.
Même discussion que pour export/import.
> Ça permettrait d'avoir des déclarations d'utilisation associées à
> chaque fichier source plutôt qu'aller les mettre dans la description
> du paquetage. Avec l'inconvénient qui va avec : c'est moins
> centralisé, donc tout n'est pas décrit au même endroit.
Et ça dépend de l'ordre de chargement des fichiers, etc.
L'état de l'art, c'est de déclarer ces dépendences une fois pour toutes
dans la forme defpackage.
On peut avoir des cas compliqués où il faut établir ces liens au moment
du chargement ou de l'exécution, mais dans les programmes normaux, la
gestion du projet sera plus simple si c'est déclaré dans le defpackage.
Il faut voir que le découpage du code en fichiers n'a pas d'importance
en général et en lisp en particulier. En C/C++, les fichiers ont une
petite importance à cause du pre-processeur C et de #include, qui
travaille sur des fichiers. Mais dans tous les autres languages, s'il
peut y avoir une notion d'unité de compilation
(CL:WITH-COMPILATION-UNIT), il n'y a pas de notion de fichier source.
Les compilateurs peuvent introduire un lien entre un élément du langage
et un fichier source. Par exemple, en java, je crois qu'un paquetage
nommé com.informatimago.myapp.myclass va être cherché par le compilateur
dans un fichier $SRC/com/informatimago/myapp/myclass.java; mais ce n'est
pas une notion du langage proprement dit. Si on avait un compilateur
java qui fonctionnait sur un système sans fichier (il y en a), la
correspondance entre le nom du paquetage et l'objet contenant le source
de ce paquetage se ferait sans faire intervenir la notion de fichier.
En CL, au niveau du langage il n'y a donc pas vraiment de notion de
fichier source, hormis le fait que COMPILE-FILE prend un
pathname-designator (pour nommer un fichier source, mais ça peut être un
file-stream!).
On pourrait dire que:
(with-compilation-unit
(compile nil '(defun …))
…)
est plus fondamental.
C'est d'ailleurs pour ça qu'on a souvent des difficultés à identifier
une ligne source pour les erreurs de programme.
Dans le cadre d'un "IDE", ne serait-ce que slime, on peut sauter d'une
définition à l'autre avec M-. slime ouvre automatiquement les fichiers
sources. Mais il marcherait aussi bien si tout le code était dans un
seul fichier. emacs peut sans problème ouvrir des fichiers de 50 mega
caractères (à condition de les répartir sur des lignes suffisament
courtes), ce qui représente toutes les sources de Firefox la dernière
fois que j'ai regardé sa taille. Comme on développe en lisp de façon
interactive, quel serait l'intérêt de répartir le code en petit
fichiers? Sur unics, c'était intéressant car le PDP-7 n'avait pas assez
de mémoire pour éditer plus d'une paire de fonctions, donc quasiment
chaque fonction du système et de la bibliothèque était dans son propre
fichier! Mais nous n'en sommes plus là! Alors?
En fait en lisp, nous avons les macros, qui peuvent avoir besoin
d'utiliser à la compilation des fonctions définies dans le programme.
Ces fonctions doivent donc être définie dans l'environnement de
compilation. On peut le faire avec (eval-when (:compile-toplevel) …).
Mais si on utilise dans ces fonctions d'autres macros qui utilisent
d'autres fonctions, etc, on fini par tout mettre dans (eval-when
(:compile-toplevel :load-toplevel :execute) …), et ce n'est pas beau.
Donc on choisi souvent de découper les sources lisp en fichiers selon la
phase où on en aura besoin, c'est à dire, l'ordre dans lequel on doit
les compiler et charger.
paquetages.lisp
fonctions-utilises-par-les-macros-1.lisp
macros-1.lisp
fonctions-utilisant-les-macros-1-utilises-par-les-macros-2.lisp
macros-2.lisp
fonctions-utilisant-les-macros-2.lisp
etc.
Bien sur, on peut souvent mélanger les fonctions et macros dans les
fichiers, et on utilise normalement des noms plus significatifs,
correspondant aux modules ou sous-modules fonctionnels. Mais la seule
raison technique de séparer le code en fichiers différents reste la
faciliter de les compiler et charger dans le bon ordre. Que l'on défini
dans un fichier .asd ;-)
Donc si on a juste deux ou trois fonctions (ou autres définitions)
nécessaires dans un macro au moment de l'expansion de la macro, on va
les mettre dans un (eval-when (:compile-toplevel) …), mais si la ou les
macros nécessitent plus de code (par exemple, tout une implémentation
d'un langage spécifique de domaine (DSL)), on va le mettre dans un
module séparé, et donc dans un fichier séparé, que l'on prendra soin de
compiler et de charger avant le fichier contenant cette macro.
(Mais encore une fois, si on avait un IDE offrant un interface
utilisateur aisé pour structurer et naviger dans le code, il peut
parfaitement utiliser un seul fichier (si fichier il y a)).
Maintenant, si tu as besoin de déclarations dans les fichiers, tu peux
toujours utiliser DECLAIM! En particulier, il y a la déclaration
DECLARATION qui permet de déclarer ses propres déclarations :-)
Tu peux ainsi déclarer n'importe quoi, et implémenter des outils lisant
tes fichiers sources, et prenant compte de ces déclarations.
Ainsi, dans les fichiers sources on trouve parfois des formes comme:
(in-package "COMMON-LISP-USER")
(declaim (declaration also-use-packages))
(declaim (also-use-packages "SOCKET" "REGEXP"))
(avant toute forme DEFPACKAGE).
La première indique au compilateur que cl-user::also-use-packages est une
déclaration.
La seconde (par convention personnelle), indique qu'il y a dans le
fichier des symboles qualifiés venant des ces paquetages, ou que le code
dans ce fichier utilises ces paquetages à l'exécution
(eg. (INTERN name package)).
Ainsi, un outil qui feuillette mes fichiers peut facilement déterminer
toutes les dépendences de paquetage, en s'arrêtant dès qu'il à trouvé
une forme DEFPACKAGE, en tenant compte de cette déclaration
ALSO-USE-PACKAGE.
On peut inventer ainsi des déclarations pour n'importe quoi.
(declaim (declaration jira-task specifications test))
(declaim (specifications "This module must do something interesting."))
(defun f ()
(declare (jira-task "XYZ-9876")
(specifications "The function F must return something
interesting.")
(test (interestingp (f))))
'boring)
et alors avoir des outils qui extraient ces déclarations, effectuent des
tests automatiques, et s'ils tombent en erreur, réouvrent la tâche jira
avec le rapport de bogue.