Если вам нужен только статически типизированный язык, который выглядит как Lisp, вы можете сделать это довольно легко, определив абстрактное синтаксическое дерево, представляющее ваш язык, и затем отобразив этот AST в S-выражениях. Тем не менее, я не думаю, что назвал бы результат Lisp.
Если вам нужно что-то, что на самом деле имеет характеристики Lisp-y, кроме синтаксиса, это можно сделать с помощью статически типизированного языка. Однако в Lisp есть много характеристик, из которых трудно получить много полезной статической типизации. Чтобы проиллюстрировать это, давайте взглянем на саму структуру списка, называемую cons , которая образует основной строительный блок Lisp.
Называть минусы списком, хотя (1 2 3)
выглядит как единое целое, немного ошибочно. Например, он совсем не похож на статически типизированный список, такой как C ++ std::list
или список Хаскелла. Это одномерные связанные списки, в которых все ячейки относятся к одному типу. Лисп с радостью разрешает (1 "abc" #\d 'foo)
. Кроме того, даже если вы расширяете свои статические списки для охвата списков списков, тип этих объектов требует, чтобы каждый элемент списка был подсписком. Как бы вы представили ((1 2) 3 4)
в них?
Лисп-конусы образуют бинарное дерево с листьями (атомами) и ветвями (консусами). Кроме того, листья такого дерева могут вообще содержать любой атомарный (не минус) тип Lisp! Гибкость этой структуры - вот что делает Lisp таким хорошим в обработке символьных вычислений, AST и преобразовании самого кода Lisp!
Так как бы вы смоделировали такую структуру в статически типизированном языке? Давайте попробуем это в Haskell, который имеет чрезвычайно мощную и точную систему статических типов:
type Symbol = String
data Atom = ASymbol Symbol | AInt Int | AString String | Nil
data Cons = CCons Cons Cons
| CAtom Atom
Ваша первая проблема будет связана с областью типа Atom. Ясно, что мы не выбрали тип Atom, обладающий достаточной гибкостью, чтобы охватить все типы объектов, которые мы хотим перебрасывать в контейнерах. Вместо того, чтобы пытаться расширить структуру данных Atom, как указано выше (которую вы можете ясно увидеть, хрупко), скажем, у нас был класс магического типа Atomic
, который отличал все типы, которые мы хотели сделать атомарными. Тогда мы можем попробовать:
class Atomic a where ?????
data Atomic a => Cons a = CCons Cons Cons
| CAtom a
Но это не сработает, потому что требуется, чтобы все атомы в дереве были одного типа . Мы хотим, чтобы они могли различаться от листа к листу. Для лучшего подхода необходимо использовать экзистенциальные квантификаторы Haskell :
class Atomic a where ?????
data Cons = CCons Cons Cons
| forall a. Atomic a => CAtom a
Но теперь вы подошли к сути вопроса. Что вы можете сделать с атомами в такой структуре? Какую общую структуру они имеют, которую можно смоделировать с помощью Atomic a
? Какой уровень безопасности типов вам гарантирован для такого типа? Обратите внимание, что мы не добавили никаких функций в наш класс типов, и есть веская причина: атомы не имеют ничего общего в Лиспе. Их супертип в Лиспе просто называется t
(то есть top).
Чтобы использовать их, вам нужно было бы придумать механизмы для динамического приведения значения атома к чему-то, что вы действительно можете использовать. И в этот момент вы в основном реализовали динамически типизированную подсистему в своем статически типизированном языке! (Нельзя не отметить возможное следствие Десятого правила программирования Гринспуна .)
Обратите внимание, что Haskell обеспечивает поддержку именно такой динамической подсистемы с типом Obj
, используемой совместно с типом Dynamic
и классом Typeable для замены нашей Atomic
класс, позволяющий хранить произвольные значения вместе с их типами, и явное приведение обратно к этим типам. Именно такую систему вам нужно использовать для работы со структурами Lisp-конов в их полной общности.
Что вы также можете сделать, это пойти другим путем и встроить статически типизированную подсистему в по существу динамически типизированный язык.Это позволяет вам использовать статическую проверку типов для частей вашей программы, которые могут использовать более строгие требования к типу.Это, кажется, подход, принятый в ограниченной форме CMUCL, например, точной проверки типов .
Наконец, есть возможность иметь две отдельные подсистемы, динамически и статически типизированные, которые используют контрактстиль программирования, чтобы помочь перемещаться между переходами.Таким образом, язык может приспосабливаться к использованию Lisp, где статическая проверка типов была бы скорее помехой, чем помощью, а также к случаям, когда статическая проверка типов была бы выгодна.Это подход, принятый Typed Racket , как вы увидите из последующих комментариев.