Si tout ce que vous vouliez était un langage statiquement typé qui ressemblait à Lisp, vous pouvez le faire assez facilement, en définissant un arbre syntaxique abstrait qui représente votre langage, puis en faisant correspondre cet AST aux expressions S. Cependant, je ne pense pas que j'appellerais le résultat Lisp.
Si vous voulez quelque chose qui a les caractéristiques de Lisp en plus de la syntaxe, il est possible de le faire avec un langage statiquement typé. Cependant, il existe de nombreuses caractéristiques de Lisp qui sont difficiles à utiliser pour le typage statique. Pour illustrer cela, regardons la structure de liste elle-même, appelée la structure contre qui constitue le principal élément de base de Lisp.
Appeler les contre une liste, cependant (1 2 3)
en a l'air, est un peu un terme impropre. Par exemple, il n'est pas du tout comparable à une liste typée statiquement, comme la liste C++. std::list
ou la liste de Haskell. Il s'agit de listes liées unidimensionnelles où toutes les cellules sont du même type. Lisp permet heureusement (1 "abc" #\d 'foo)
. De plus, même si vous étendez vos listes à typage statique pour couvrir les listes de listes, le type de ces objets exige que chaque L'élément de la liste est une sous-liste. Comment représenter ((1 2) 3 4)
en eux ?
Les conses de Lisp forment un arbre binaire, avec des feuilles (atomes) et des branches (conses). De plus, les feuilles d'un tel arbre peuvent contenir n'importe quel type Lisp atomique (non-cons) ! La flexibilité de cette structure est ce qui rend Lisp si bon pour gérer le calcul symbolique, les ASTs, et la transformation du code Lisp lui-même !
Alors comment modéliser une telle structure dans un langage statiquement typé ? Essayons en Haskell, qui possède un système de types statiques extrêmement puissant et précis :
type Symbol = String
data Atom = ASymbol Symbol | AInt Int | AString String | Nil
data Cons = CCons Cons Cons
| CAtom Atom
Votre premier problème sera la portée du type Atom. Il est clair que nous n'avons pas choisi un type Atom suffisamment flexible pour couvrir tous les types d'objets que nous voulons faire circuler dans les conses. Au lieu d'essayer d'étendre la structure de données Atom comme indiqué ci-dessus (ce qui, comme vous pouvez le constater, est fragile), disons que nous avons une classe de type magique Atomic
qui distinguait tous les types que nous voulions rendre atomiques. Alors nous pourrions essayer :
class Atomic a where ?????
data Atomic a => Cons a = CCons Cons Cons
| CAtom a
Mais cela ne fonctionnera pas car il faut que tous les atomes de l'arbre soient de la catégorie même type. Nous voulons qu'ils puissent différer d'une feuille à l'autre. Une meilleure approche nécessite l'utilisation de la fonction quantificateurs existentiels :
class Atomic a where ?????
data Cons = CCons Cons Cons
| forall a. Atomic a => CAtom a
Mais vous arrivez maintenant au cœur du problème. Que pouvez-vous faire avec des atomes dans ce type de structure ? Quelle est la structure qu'ils ont en commun et qui pourrait être modélisée avec Atomic a
? Quel est le niveau de sécurité de type garanti avec un tel type ? Notez que nous n'avons pas ajouté de fonctions à notre classe de types, et ce pour une bonne raison : les atomes n'ont rien en commun en Lisp. Leur supertype en Lisp est simplement appelé t
(c'est-à-dire en haut).
Pour pouvoir les utiliser, il faudrait trouver des mécanismes pour contraindre dynamiquement la valeur d'un atome à quelque chose que vous pouvez réellement utiliser. Et à ce stade, vous avez fondamentalement implémenté un sous-système dynamiquement typé dans votre langage statiquement typé ! (On ne peut s'empêcher de noter un possible corollaire à La dixième règle de programmation de Greenspun .)
Notez que Haskell fournit un support pour une telle fonction sous-système dynamique avec un Obj
utilisé en conjonction avec un Dynamic
et un Classe de type pour remplacer notre Atomic
qui permettent de stocker des valeurs arbitraires avec leurs types, et une coercition explicite à partir de ces types. C'est le genre de système qu'il faudrait utiliser pour travailler avec les structures de cons Lisp dans toute leur généralité.
Vous pouvez également faire l'inverse et intégrer un sous-système à typage statique dans un langage essentiellement à typage dynamique. Cela vous permet de bénéficier de la vérification statique des types pour les parties de votre programme qui peuvent tirer parti d'exigences de type plus strictes. Cela semble être l'approche adoptée dans la forme limitée de CMUCL de un contrôle précis des types par exemple.
Enfin, il y a la possibilité d'avoir deux sous-systèmes séparés, typés dynamiquement et statiquement, qui utilisent une programmation de type contrat pour faciliter la transition entre les deux. De cette façon, le langage peut s'adapter à des utilisations de Lisp où la vérification de type statique serait plus une gêne qu'une aide, ainsi qu'à des utilisations où la vérification de type statique serait avantageuse. C'est l'approche adoptée par Racket typé comme vous le verrez dans les commentaires qui suivent.
15 votes
J'aime les macros libres de Lisp, mais j'aime la robustesse du système de types de Haskell. J'aimerais voir à quoi ressemble un Lisp à typage statique.
6 votes
Bonne question ! Je crois shenlanguage.org fait ça. J'aimerais que ça devienne plus courant.
2 votes
github.com/carp-lang/Carp
1 votes
Comment faire du calcul symbolique avec Haskell ? (solve 'x '(= (+ x y) (* x y))). Si vous le mettez dans une chaîne, il n'y a pas de vérification (contrairement à Lisp qui peut utiliser des macros pour ajouter une vérification). Si vous utilisez des types de données algébriques ou des listes... Ce sera très verbeux : solve (Sym "x") (Eq (Plus (Sym "x") (Sym "y")) (Mult (Sym "x") (Sym "y")))