Возможно ли использовать статически типизированный полный вариант Lisp? Имеет ли смысл иметь что-то подобное? Я считаю, что одним из достоинств языка Lisp является простота его определения. Стабильная типизация компрометирует этот основной принцип?
Возможна ли статически типизированная полная версия Lisp?
Ответ 1
Да, это очень возможно, хотя стандартная система типа HM обычно является неправильным выбором для большинства идиоматических Lisp/Scheme-кода. См. Typed Racket для недавнего языка, который является "Полный Lisp" (более похожий на Scheme) со статической типизацией.
Ответ 2
Если вам нужен только статически типизированный язык, похожий на Lisp, вы можете сделать это довольно легко, определив абстрактное синтаксическое дерево, представляющее ваш язык, и затем отобразив этот AST в S-выражениях. Тем не менее, я не думаю, что назвал бы результат Lisp.
Если вам нужно что-то, что на самом деле имеет характеристики Lisp-y, кроме синтаксиса, это можно сделать с помощью статически типизированного языка. Однако в Lisp есть много характеристик, из которых трудно получить много полезной статической типизации. Чтобы проиллюстрировать это, давайте взглянем на саму структуру списка, которая называется cons, которая образует основной строительный блок Lisp.
Называть минусы списком, хотя (1 2 3)
выглядит как единое целое, немного ошибочно. Например, он совсем не похож на статически типизированный список, такой как C++ std::list
или список Haskell. Это одномерные связанные списки, в которых все ячейки относятся к одному типу. Lisp с радостью разрешает (1 "abc" #\d 'foo)
. Кроме того, даже если вы расширяете свои статические списки для охвата списков списков, тип этих объектов требует, чтобы каждый элемент списка был подсписком. Как бы вы представили в них ((1 2) 3 4)
?
Lisp-конусы образуют бинарное дерево с листьями (атомы) и ветвями (консы). Кроме того, листья такого дерева могут вообще содержать любой атомарный (не минус) тип 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
? Какой уровень безопасности типов вам гарантирован для такого типа? Обратите внимание, что мы не добавили никаких функций в наш класс типов, и есть веская причина: атомы не имеют ничего общего в Lispе. Их супертип в Lispе просто называется t
(т.е. top).
Чтобы использовать их, вам нужно было бы придумать механизмы для динамического приведения значения атома к тому, что вы действительно можете использовать. И в этот момент вы в основном реализовали динамически типизированную подсистему в своем статически типизированном языке! (Нельзя не отметить возможное следствие Десятого правила программирования Гринспена.)
Обратите внимание, что Haskell обеспечивает поддержку именно такой динамической подсистемы с типом Obj
, используемой совместно с типом Dynamic
и классом Typeable для замены нашего класса Atomic
, которые позволяют хранить произвольные значения вместе с их типами и явно приводить их обратно. Что за тип системы, который вам нужно использовать, чтобы работать со структурами Lisp в полном обобщении.
То, что вы также можете сделать, это пойти другим путем и встроить статически типизированную подсистему в существенно динамически типизированный язык. Это позволяет вам использовать статическую проверку типов для частей вашей программы, которые могут использовать более строгие требования к типу. Похоже, что такой подход применяется в ограниченной форме CMUCL, например, для точной проверки типов.
Наконец, существует возможность иметь две отдельные подсистемы, динамически и статически типизированные, которые используют программирование в стиле контракта, чтобы помочь в переходе между этими двумя. Таким образом, язык может приспосабливаться к использованию Lisp, где статическая проверка типов была бы скорее помехой, чем помощью, а также к случаям, когда статическая проверка типов была бы выгодна. Это подход, принятый Typed Racket, как вы увидите из последующих комментариев.
Ответ 3
Мой ответ, без высокой степени уверенности, вероятно. Если вы посмотрите на язык, например, SML, и сравните его с Lisp, функциональное ядро каждого из них почти идентично. В результате не похоже, что у вас возникнут проблемы с применением какого-то статического набора текста в ядре Lisp (функциональное приложение и примитивные значения).
Ваш вопрос действительно говорит об этом, и, когда я вижу, что некоторые из проблем возникают, это подход, основанный на использовании кода. Типы существуют на более абстрактном уровне, чем выражения. Lisp не имеет этого различия - все "плоское" по структуре. Если мы рассмотрим некоторое выражение E: T (где T - некоторое представление его типа), а затем мы рассмотрим это выражение как простые данные, то что же такое тип T здесь? Ну, это мило! Вид более высокий, тип заказа, поэтому давайте просто поговорим об этом в нашем коде:
E : T :: K
Вы можете увидеть, где я собираюсь с этим. Я уверен, отделив информацию о типе от кода, можно было бы избежать такой самореференциальности типов, однако это сделало бы типы не очень "lisp" в их вкусе. Вероятно, есть много способов обойти это, хотя для меня это не очевидно, что было бы лучшим.
EDIT: О, так что с небольшим количеством googling я нашел Qi, который, похоже, очень похож на Lisp за исключением что он статически типизирован. Возможно, это хорошее место, чтобы начать видеть, где они вносили изменения, чтобы получить статическую печать там.
Ответ 4
Dylan: расширение системы типов Dylan для лучшего вывода типов и обнаружения ошибок