Возможен ли статически типизированный полный вариант Лиспа?

107

Возможен ли статически типизированный полный вариант Лиспа? Есть ли вообще смысл в существовании чего-то подобного? Я считаю, что одним из достоинств языка Lisp является простота его определения. Подорвет ли статическая типизация этот основной принцип?

Лямбда Предпоследняя
источник
10
Мне нравятся макросы произвольной формы в Lisp, но мне нравится надежность системы типов Haskell. Мне бы хотелось посмотреть, как выглядит статически типизированный Лисп.
mcandre
4
Хороший вопрос! Я считаю, что это делает shenlanguage.org . Я бы хотел, чтобы это стало более популярным.
Хэмиш Грубиджан
Как вы выполняете символические вычисления с помощью Haskell? (решить 'x' (= (+ xy) (* xy))). Если вы поместите его в строку, проверки не будет (в отличие от Lisp, который может использовать макросы для добавления проверки). Если вы используете алгебраические типы данных или списки ... Это будет очень многословно: решить (Sym «x») (Eq (Plus (Sym «x») (Sym «y»)) (Mult (Sym «x») (Sym "y")))
aoeu256 04

Ответы:

57

Да, это очень возможно, хотя стандартная система типов в стиле HM обычно является неправильным выбором для большинства идиоматического кода Lisp / Scheme. См. Typed Racket, чтобы узнать о новом языке, который является «Full Lisp» (на самом деле больше похожим на Scheme) со статической типизацией.

Эли Барзилай
источник
1
Проблема в том, что это за тип списка, который составляет весь исходный код типизированной программы для рэкетов?
Zorf
18
Обычно так и бывает Sexpr.
Эли Барзилай
Но тогда я могу писать coerce :: a->bв терминах eval. Где безопасность типов?
ssice
2
@ssice: когда вы используете нетипизированную функцию, например, evalвам нужно проверить результат, чтобы увидеть, что получится, что не является новым в Typed Racked (то же самое, что и функция, которая принимает тип объединения Stringи Number). Неявный способ увидеть, что это возможно, заключается в том, что вы можете написать и использовать язык с динамической типизацией на языке с статической типизацией HM.
Эли Барзилай
37

Если все, что вам нужно, это статически типизированный язык, похожий на Lisp, вы могли бы сделать это довольно легко, определив абстрактное синтаксическое дерево, представляющее ваш язык, и затем сопоставив этот AST с S-выражениями. Однако я не думаю, что назову результат Лиспом.

Если вам нужно что-то, что действительно имеет характеристики Lisp-y, помимо синтаксиса, это можно сделать с помощью статически типизированного языка. Однако у Лиспа есть много характеристик, из которых трудно извлечь много полезной статической типизации. Чтобы проиллюстрировать это, давайте взглянем на саму структуру списка, называемую cons , которая формирует основной строительный блок Lisp.

Называть минусы списком, хотя и (1 2 3)выглядит так, но неправильно. Например, он совершенно не сопоставим со статически типизированным списком, таким как список C ++ std::listили Haskell. Это одномерные связанные списки, в которых все ячейки одного типа. Лисп с радостью позволяет (1 "abc" #\d 'foo). Кроме того, даже если вы расширяете свои списки со статической типизацией до списков-списков, тип этих объектов требует, чтобы каждый элемент списка был подсписком. Как бы вы ((1 2) 3 4)в них представились ?

Конусы Лиспа образуют двоичное дерево с листьями (атомами) и ветвями (конусами). Кроме того, листья такого дерева могут содержать любой атомарный (не являющийся минусом) тип Лиспа! Гибкость этой структуры делает 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(т.е. верхний).

Чтобы использовать их, вам нужно было бы разработать механизмы для динамического приведения значения атома к тому, что вы действительно можете использовать. И в этот момент вы в основном реализовали подсистему с динамической типизацией в своем статически типизированном языке! (Нельзя не отметить возможное следствие десятого правила программирования Гринспана .)

Обратите внимание, что Haskell обеспечивает поддержку именно такой динамической подсистемы с Objтипом, используемым в сочетании с Dynamicтипом и классом Typeable для замены нашего Atomicкласса, что позволяет сохранять произвольные значения с их типами и явным приведением обратно из этих типов. Это та система, которую вам нужно использовать для работы со структурами cons в Лиспе во всей их общности.

Что вы также можете сделать, так это пойти другим путем и встроить статически типизированную подсистему в по существу динамически типизированный язык. Это дает вам преимущество проверки статического типа для частей вашей программы, которые могут использовать преимущества более строгих требований к типу. Похоже, что это подход, используемый, например, в ограниченной форме точной проверки типов в CMUCL .

Наконец, есть возможность иметь две отдельные подсистемы, динамически и статически типизированные, которые используют программирование в контрактном стиле, чтобы облегчить переход между ними. Таким образом, язык может приспособиться к использованию Лиспа, где проверка статического типа будет больше помехой, чем помощью, а также использования, где проверка статического типа была бы полезной. Это подход, используемый Typed Racket , как вы увидите из последующих комментариев.

Оуэн С.
источник
16
Этот ответ страдает от фундаментальной проблемы: вы предполагаете, что системы статических типов должны быть в стиле HM. Основная концепция, которая не может быть выражена здесь и является важной особенностью кода Lisp, - это создание подтипов. Если вы посмотрите на набранную ракетку, вы увидите, что она может легко отображать любой список, включая такие вещи, как (Listof Integer)и (Listof Any). Очевидно, вы подозреваете, что последнее бесполезно, потому что вы ничего не знаете о типе, но в TR вы можете позже использовать, (if (integer? x) ...)и система будет знать, что xэто целое число в 1-й ветви.
Эли Барзилай,
5
Да, и это плохая характеристика типизированной ракетки (которая отличается от ненадежных систем типов, которые вы можете найти в некоторых местах). Typed Racket - это язык со статической типизацией , без дополнительных затрат времени выполнения для типизированного кода. Racket по-прежнему позволяет писать часть кода на TR, а часть на обычном нетипизированном языке - и для этих случаев используются контракты (динамические проверки) для защиты типизированного кода от потенциально некорректного нетипизированного кода.
Эли Барзилай,
1
@Eli Barzilay: Я солгал, здесь четыре части: 4. Мне интересно, как принятый в отрасли стиль кодирования C ++ постепенно уходит от подтипов к обобщениям. Слабость заключается в том, что язык не предоставляет помощи для объявления интерфейса, который будет использовать универсальная функция, в чем, безусловно, могут помочь классы типов. Кроме того, C ++ 0x может добавлять вывод типа. Не HM, я полагаю, но ползет в том направлении?
Оуэн С.
1
Оуэн: (1) суть в том, что вам нужны подтипы для выражения того типа кода, который пишут лисперы - а этого просто не может быть с системами HM, поэтому вы вынуждены использовать настраиваемые типы и конструкторы для каждого использования, которые делает использование всего этого более неудобным. В типизированном рэкете использование системы с подтипами было следствием намеренного дизайнерского решения: результат должен иметь возможность выражать типы такого кода без изменения кода или создания пользовательских типов.
Эли Барзилай
1
(2) Да, dynamicтипы становятся популярными в статических языках как своего рода обходной путь для получения некоторых преимуществ динамически типизированных языков с обычным компромиссом, заключающимся в том, что эти значения обертываются таким образом, чтобы сделать типы идентифицируемыми. Но и здесь типизированный рэкет очень хорошо справляется с задачей сделать его удобным в рамках языка - средство проверки типов использует вхождения предикатов, чтобы узнать больше о типах. Например, посмотрите напечатанный пример на странице рэкет и посмотрите, как string?«сокращает» список строк и чисел до списка строк.
Эли Барзилай
10

Мой ответ, без особой уверенности, наверное . Если вы посмотрите, например, на такой язык, как SML, и сравните его с Lisp, функциональное ядро ​​каждого из них практически идентично. В результате не похоже, что у вас будет много проблем с применением статической типизации к ядру Lisp (приложение функций и примитивные значения).

Ваш вопрос действительно говорит полный , и где я вижу, что некоторые из проблем возникают, это подход «код как данные». Типы существуют на более абстрактном уровне, чем выражения. Lisp не имеет этого различия - все «плоское» по структуре. Если мы рассмотрим какое-то выражение E: T (где T - некоторое представление его типа), а затем мы будем рассматривать это выражение как простые старые данные, то что именно здесь является типом T? Ну это вид! Тип - это тип более высокого порядка, поэтому давайте просто скажем что-нибудь об этом в нашем коде:

E : T :: K

Вы можете увидеть, к чему я клоню. Я уверен, что, отделив информацию о типе от кода, можно было бы избежать такого рода самореферентности типов, однако это сделало бы типы не слишком "шепелявыми" в своем вкусе. Вероятно, есть много способов обойти это, хотя для меня не очевидно, какой из них будет лучшим.

РЕДАКТИРОВАТЬ: Итак, немного погуглив, я нашел Qi , который очень похож на Lisp, за исключением того, что он статически типизирован. Возможно, это хорошее место, чтобы увидеть, где они внесли изменения, чтобы добавить статическую типизацию.

Джиан
источник
Кажется, что следующая итерация после Ци - это Шэнь , разработанная тем же человеком.
Diagon
4

Дилан: Расширение системы типов Дилана для лучшего вывода типов и обнаружения ошибок

Райнер Йосвиг
источник
Ссылка мертвая. Но в любом случае Дилан не имеет статической типизации.
Бьорн Линдквист
@ BjörnLindqvist: эта ссылка была на тезис о добавлении постепенного набора текста для Дилана.
Райнер
1
@ BjörnLindqvist: Я дал ссылку на обзорную статью.
Райнер Йосвиг
Но постепенный набор текста не считается статическим. Если бы это было так, то Pypy был бы статически типизированным Python, поскольку он также использует постепенную типизацию.
Бьорн Линдквист
2
@ BjörnLindqvist: если мы добавляем статические типы с помощью постепенной типизации, и они проверяются во время компиляции, то это статическая типизация. Просто не вся программа статически типизирована, а части / регионы. homes.sice.indiana.edu/jsiek/what-is-gradual-typing «Постепенная типизация - это система типов, которую я разработал вместе с Валидом Тахой в 2006 году, которая позволяет динамически типизировать части программы, а другим - статически».
Райнер