Il y a quelque temps (environ trois ans), j'ai décidé de lire un manuel sur Lisp. Sans but précis, juste pour le développement général et la capacité de choquer les interlocuteurs avec de l'exotisme (une fois que cela semble, ça a même fonctionné).
Mais en y regardant de plus près, Lisp s'est avéré être vraiment puissant, flexible et, curieusement, utile dans la vie quotidienne. Toutes les tâches d'automatisation mineures ont rapidement migré vers des scripts en Lisp, et il y avait également des possibilités d'automatisation de tâches plus complexes.
Il convient de noter ici que par «capacité d'automatisation», je veux dire une situation où le temps total pour écrire et déboguer un programme est inférieur au temps passé à résoudre manuellement la même tâche.
Paul Graham a écrit plus d'un article et même un livre sur les avantages de Lisp. Au moment d'écrire ces lignes, Lisp est classé 33ème dans le classement TOIBE (trois fois mort que Delphi mort). La question se pose: pourquoi la langue est-elle si petite si elle est si pratique? Environ deux ans d'utilisation ont donné quelques indications sur les raisons.
Inconvénients
1. Structures de données partagéesUn concept qui vous permet d'optimiser des programmes fonctionnels, mais lourd d'erreurs subtiles en impératif. La possibilité de dommages accidentels à une structure de données superflue lorsqu'une variable qui n'a pas de connexion visible avec la structure nécessite que le programmeur surveille constamment ce qui se passe dans les coulisses et connaisse l'implémentation interne de chaque fonction utilisée (système et utilisateur). La chose la plus étonnante est la possibilité d'endommager le corps d'une fonction en modifiant sa valeur de retour.
2. Manque d'encapsulationBien que le concept de package existe, il n'a rien à voir avec le
package dans Ada ou l'
unité dans Delphi. Tout code peut ajouter n'importe quoi à n'importe quel package (sauf ceux du système). Tout code peut extraire n'importe quoi de n'importe quel package en utilisant l'opérateur
:: .
3. Abréviations aléatoiresQuelle est la différence entre MAPCAN et MAPCON? Pourquoi dans SETQ, la dernière lettre Q? Compte tenu de l'âge de la langue, vous pouvez comprendre les raisons de cet état de fait, mais je veux que la langue soit un peu plus propre.
4. MultithreadingCet inconvénient est indirectement lié à Lisp et concerne principalement l'implémentation que j'utilise - SteelBank Common Lisp. Common Lisp ne prend pas en charge le multithreading. Une tentative d'utilisation de l'implémentation fournie par SBCL a échoué.
Il est dommage de refuser un outil aussi pratique, mais l'insatisfaction s'accumule progressivement.
Rechercher une solution
Vous pouvez d'abord aller sur Wikipedia sur la page Lisp. Inspectez la section "Dialectes". Lisez une brève introduction à chacun. Et réalisez que le goût et la couleur de tous les marqueurs sont différents.
Si vous voulez faire quelque chose, vous devez le faire vous-même
- Jean Baptiste Emmanuel Sorg
Essayons de créer notre propre Lisp correct en y ajoutant un peu d'Ada, beaucoup de Delphi et une goutte d'Oberon. Nous appelons le mélange résultant Fox.
Concepts de base
1. Pas de pointeursDans la lutte contre PROBLEM-1, toutes les opérations doivent être effectuées en copiant les valeurs. Par le type de structure de données dans le code ou lors de l'impression, toutes ses propriétés, connexions externes et internes doivent être entièrement visibles.
2. Ajouter des modulesDans le cadre de la lutte contre le problème 2, nous importons des
packages ,
avec et
utilisons des déclarations d'Ada. Dans le processus, nous rejetons le schéma d'importation / d'observation trop complexe pour les symboles Lisp.
(package - ( ) () ())
(with -)
(use -)
3. Moins d'abréviationsLes caractères les plus courants et les plus courants seront toujours abrégés, mais surtout les plus évidents:
const ,
var . Fonction de sortie formatée - FMT nécessite une réduction, car elle se trouve souvent à l'intérieur des expressions.
Elt - prenant un élément - a fui de Common Lisp et a pris racine, bien qu'il ne soit pas nécessaire de le réduire.
4. Identifiants insensibles à la casseJe pense que la langue (et le système de fichiers) corrects {$ HOLYWAR +} devrait être insensible à la casse {$ HOLYWAR-} afin de ne pas lui faire une fois de plus la cervelle.
5. Facilité d'utilisation avec la disposition du clavier russeLa syntaxe Lisi évite de toutes les manières possibles l'utilisation de caractères qui ne sont pas disponibles dans l'une des mises en page. Pas d'accolades carrées ou bouclées. Non #, ~, &, <,>, |. Lors de la lecture de littéraux numériques, une virgule et un point sont considérés comme des séparateurs décimaux.
6. Alphabet étenduUne des bonnes choses à propos de SBCL était UTF-8 dans le code. La possibilité de déclarer les constantes BEAR, VODKA et BALALAYKA simplifie grandement l'écriture du code d'application. La possibilité d'insérer Ω, Ψ et Σ rend les formules dans le code plus visuelles. Bien qu'il soit théoriquement possible d'utiliser des caractères Unicode, il est difficile de garantir l'exactitude du travail avec eux (plutôt paresse que difficile). Nous nous limitons au cyrillique, au latin et au grec.
7. Littéraux numériquesC'est l'extension linguistique la plus utile pour moi.
10_000
Cette dernière option me semble la plus peu esthétique, mais elle est la plus populaire.
8. CyclesLes cycles en Lisp sont non standard et assez désordonnés. Simplifiez à l'ensemble standard minimum.
(for i 5
La variable de boucle n'est pas visible à l'extérieur.
(while )
9. GOTOPas un opérateur très nécessaire, mais sans lui, il est difficile de démontrer la négligence des règles de programmation structurelle.
(block : (goto :))
10. Unification du champ d'applicationIl existe deux types différents de portée dans Lisp: TOPLEVEL et local. En conséquence, il existe deux façons différentes de déclarer des variables.
(defvar A 1) (let ((a 1)) …)
Dans Fox, il n'y a qu'une seule méthode utilisée à la fois au niveau supérieur du script et dans les zones locales, y compris les packages.
(var A 1)
Si vous souhaitez limiter la portée, utilisez l'opérateur
(block (var A 1) (set A 2) (fmt nil A))
Le corps de la boucle est contenu dans l'instruction BLOCK implicite (comme le corps de la fonction / procédure). Toutes les variables déclarées dans la boucle sont détruites à la fin de l'itération.
11. Caractères à un seul emplacementEn Lisp, les fonctions sont des objets spéciaux et sont stockées dans un emplacement de symbole spécial. Un seul caractère peut stocker simultanément une variable, une fonction et une liste de propriétés. Chez un renard, chaque personnage n'est associé qu'à une seule signification.
12. ELT pratiqueL'accès typique à un élément de structure complexe en Lisp ressemble à ceci
(elt (slot-value (elt 1) '-2) 3)
Le Fox implémente un opérateur ELT unifié qui donne accès à des éléments de tous types composites (listes, chaînes, enregistrements, tableaux d'octets, tables de hachage).
(elt 1 \-2 3)
Des fonctionnalités identiques peuvent également être obtenues avec une macro en Lisp
(defmacro field (object &rest f) " . (field *object* 0 :keyword symbol \"string\") . plist. ( ) . ." (if f (symbol-macrolet ((f0 (elt f 0))(rest (subseq f 1))) (cond ((numberp f0) `(field (elt ,object ,f0) ,@rest)) ((keywordp f0) `(field (getf ,object ,f0) ,@rest)) ((stringp f0) `(field (cdr (assoc ,f0 ,object :test 'equal)) ,@rest)) ((and (listp f0) (= 2 (length f0))) `(field (,(car f0) ,(cadr f0) ,object) ,@rest)) ((symbolp f0) `(field (,f0 ,object) ,@rest)) (t `(error " ")))) object))
13. Restriction des modes de transfert des paramètres des sous-programmesIl existe au moins cinq modes de transfert de paramètres en Lisp: obligatoire,
& facultatif ,
& reste ,
& clé ,
& entier et leur combinaison arbitraire est autorisée. En fait, la plupart des combinaisons donnent des effets étranges.
Dans Fox, il est autorisé d'utiliser uniquement une combinaison des paramètres requis et l'un des modes suivants parmi lesquels choisir
: clé ,: facultatif ,: drapeau ,: repos .
14. MultithreadingAfin de simplifier au maximum l'écriture de programmes multithreads, le concept de séparation de mémoire a été adopté. Lorsqu'un thread est généré, toutes les variables disponibles pour le nouveau thread sont copiées. Toutes les références à ces variables sont remplacées par des références à des copies. Le transfert d'informations entre les flux n'est possible que via des objets protégés ou via le résultat renvoyé par le flux une fois terminé.
Les objets protégés contiennent toujours des sections critiques pour assurer les opérations atomiques. La connexion aux sections critiques est automatique - il n'y a pas d'opérateurs séparés pour cela dans la langue. Les objets protégés incluent: file d'attente de messages, console et descripteurs de fichiers.
La création de threads est possible avec une fonction d'affichage multi-thread
(map-th (function (x) …) --)
Map-th démarre automatiquement le nombre de threads égal au nombre de processeurs du système (ou deux fois plus si vous avez Intel à l'intérieur). Dans un appel récursif, les appels de mappage suivants fonctionnent dans un seul thread.
En outre, il existe une fonction de thread intégrée qui exécute une procédure / fonction dans un thread séparé.
15. Propreté fonctionnelle dans le code impératifLe Fox a des fonctions de programmation fonctionnelle et des procédures procédurales. Les routines déclarées à l'aide du mot clé function sont soumises aux exigences de l'absence d'effets secondaires et de l'indépendance du résultat vis-à-vis des facteurs externes.
Non réalisé
Certaines fonctionnalités intéressantes de Lisp n'ont pas été satisfaites en raison d'une faible priorité.
1. Méthodes généraliséesCapacité à surcharger les fonctions avec defgeneric / defmethod.
2. Héritage3. Débogueur intégréLorsqu'une exception se produit, l'interpréteur Lisp passe en mode débogage.
4. UFFIInterface pour connecter des modules écrits dans d'autres langues.
5. BIGNUMPrise en charge de la profondeur de bits arbitraire
JetéCertaines fonctionnalités de Lisp ont été considérées et considérées comme inutiles / nuisibles.
1. Combinaison guidée de méthodesLorsqu'une méthode est appelée pour une classe, une combinaison de méthodes parentes est effectuée et il est possible de modifier les règles de combinaison. Le comportement final de la méthode semble peu prévisible.
2. RedémarreLe gestionnaire d'exceptions peut apporter des modifications à l'état du programme et envoyer une commande de redémarrage au code qui a généré l'exception. L'effet de l'application est similaire à l'utilisation de l'opérateur GOTO pour passer d'une fonction à l'autre.
3. Le récit romainLisp prend en charge le système de numérotation, qui est obsolète peu de temps avant son apparition.
Utiliser
Voici quelques exemples de code simples.
(function crc8 (data :optional seed) (var result (if-nil seed 0)) (var s_data data) (for bit 8 (if (= (bit-and (bit-xor result s_data) $01) 0) (set result (shift result -1 8)) (else (set result (bit-xor result $18)) (set result (shift result -1 8)) (set result (bit-or result $80)))) (set s_data (shift s_data -1 8))) result)
Implémentation
L'interpréteur est écrit en Delphi (FreePascal en mode compatibilité). Il est construit dans Lazarus 1.6.2 et supérieur, sous Windows et Linux 32 et 64 bits. Parmi les dépendances externes, nécessite libmysql.dll. Contient environ 15_000..20_000 lignes. Il existe environ 200 fonctions intégrées à des fins diverses (certaines sont surchargées huit fois).
Stocké iciLa prise en charge du typage dynamique est effectuée de manière triviale - tous les types de données traitées sont représentés par les héritiers de la même classe TValue.
Le type le plus important pour Lisp - la liste est, comme c'est la coutume dans Delphi, une classe contenant un tableau dynamique d'objets de type TValue. Pour ce type, le mécanisme CopyOnWrite est implémenté.
La gestion de la mémoire est automatique basée sur le comptage des références. Pour les structures récursives, tous les liens de la structure sont comptés simultanément. La libération de mémoire démarre immédiatement lorsque les variables quittent la portée. Il n'y a aucun mécanisme pour le démarrage différé du ramasse-miettes.
La gestion des exceptions fonctionne sur un mécanisme intégré à Delphi. Ainsi, les erreurs qui se produisent dans le code de l'interpréteur peuvent être traitées par le code exécutable sur le Fox.
Chaque opérateur ou fonction Lisi intégrée est implémentée en tant que méthode ou fonction dans le code d'interpréteur. Le script est exécuté par appel mutuellement récursif d'implémentations. Le code interprète et le script ont une pile d'appels commune.
Les variables de script sont stockées dans la mémoire dynamique indépendamment. Chaque fonction définie par l'utilisateur a sa propre pile pour stocker les références de variables, indépendamment de la pile de niveau supérieur ou de la pile des fonctions parentes.
La mise en œuvre de l'opérateur d'affectation (ensemble) pour les éléments structuraux est particulièrement difficile. Le calcul direct du pointeur vers l'élément requis entraîne le risque de suspendre les liens, car la syntaxe Lisi n'interdit pas de modifier la structure lors du calcul de l'élément requis. Comme solution de compromis, un «pointeur de chaîne» est implémenté - un objet contenant une référence à une variable et un tableau d'indices numériques pour indiquer le chemin dans la structure. Un tel pointeur est également sensible au problème des liens pendants, mais en cas de défaillance, il génère un message d'erreur significatif.
Outils de développement
1. Console2. Éditeur de texteÉquipé de la coloration syntaxique et de la possibilité d'exécuter un script modifiable en F9.

Conclusion
Dans l'état actuel, le projet résout les problèmes pour lesquels il a été conçu et ne nécessite pas de développement actif supplémentaire. Bon nombre des imperfections présentes n'affectent pas significativement le travail.