Я работаю над небольшим компилятором лямбда-исчисления, который имеет работающую систему логического вывода типа Хиндли-Милнера, а теперь также поддерживает рекурсивный метод давайте (не в связанном коде), который, как я понимаю, должно быть достаточно для завершения Тьюринга .
Проблема сейчас в том, что я понятия не имею, как создать списки поддержки или уже поддерживает их, и мне просто нужно найти способ их кодирования. Я хотел бы иметь возможность определять их без добавления новых правил в систему типов.
Самый простой способ, которым я могу представить список, x
- это что-то, что есть null
(или пустой список), или пара, которая содержит как x
и, так и список x
. Но для этого мне нужно уметь определять пары и или, которые, как я считаю, являются продуктом и типом суммы.
Кажется, я могу определить пары таким образом:
pair = λabf.fab
first = λp.p(λab.a)
second = λp.p(λab.b)
Так как pair
будет иметь тип a -> (b -> ((a -> (b -> x)) -> x))
, после передачи, скажем, a int
и a string
, он даст что-то с типом (int -> (string -> x)) -> x
, который будет представлением пары int
и string
. Что меня здесь беспокоит, так это то, что если это представляет пару, то почему это логически не эквивалентно и не подразумевает предложение int and string
? Тем не менее, это эквивалентно (((int and string) -> x) -> x)
, как если бы я мог иметь только типы продуктов в качестве параметров для функций. Этот ответпохоже, решают эту проблему, но я понятия не имею, что означают символы, которые он использует. Кроме того, если это на самом деле не кодирует тип продукта, могу ли я что-то сделать с типами продуктов, которые я не смог бы сделать с моим определением пар выше (учитывая, что я также могу определять n-кортежи таким же образом)? Если нет, не будет ли это противоречить тому факту, что вы не можете выразить (AFAIK) соединение, используя только подтекст?
Кроме того, как насчет типа суммы? Можно ли как-то кодировать его, используя только тип функции? Если так, будет ли этого достаточно для определения списков? Или еще, есть ли другой способ определения списков без необходимости расширять мою систему типов? И если нет, то какие изменения мне нужно было бы сделать, если я хочу сделать это как можно более простым?
Пожалуйста, имейте в виду, что я программист, но не ученый и не математик, и довольно плохо читаю математические записи.
Изменить: я не уверен, что техническое название того, что я реализовал до сих пор, но все, что у меня есть, это в основном код, который я связал выше, это алгоритм генерации ограничений, который использует правила для приложений, абстракций и взятых переменных из алгоритма Хинли-Милнера, а затем алгоритм объединения, который получает основной тип. Например, выражение \a.a
выдаст тип a -> a
, а выражение \a.(a a)
сгенерирует ошибку проверки возникновения. Помимо этого, существует не совсем let
правило, а функция, которая, по-видимому, имеет тот же эффект, который позволяет вам определять рекурсивные глобальные функции, такие как этот псевдокод:
GetTypeOfGlobalFunction(term, globalScope, nameOfFunction)
{
// Here 'globalScope' contains a list of name-value pair where every value is of class 'ClosedType',
// meaning their type will be cloned before unified in the unification algorithm so that they can be used polymorphically
tempType = new TypeVariable() // Assign a dummy type to `tempType`, say, type 'x'.
// The next line creates an scope with everything in 'globalScope' plus the 'nameOfFunction = tempType' name-value pair
tempScope = new Scope(globalScope, nameOfFunction, tempType)
type = TypeOfTerm(term, tempScope) // Calculate the type of the term
Unify(tempType, type)
return type
// After returning, the code outside will create a 'ClosedType' using the returned type and add it to the global scope.
}
Код в основном получает тип термина как обычно, но перед объединением он добавляет имя функции, определяемой с помощью фиктивного типа, в область действия типа, чтобы его можно было использовать изнутри самого себя рекурсивно.
Редактировать 2: Я только что понял, что мне также понадобятся рекурсивные типы, которых у меня нет, чтобы определить список так, как я хочу.
let func = \x -> (func x)
) вы получите то, что у меня есть.Ответы:
Пары
Эта кодировка является церковной кодировкой пар. Подобные методы могут кодировать логические значения, целые числа, списки и другие структуры данных.
x:a; y:b
pair x y
(a -> b -> t) -> t
a
b
t
pair
t
pair
является конструктором для парного типа иfirst
иsecond
являются деструкторами. (Это те же слова, которые используются в объектно-ориентированном программировании; здесь слова имеют значение, связанное с логической интерпретацией типов и терминов, которые я не буду здесь описывать.) Интуитивно, деструкторы позволяют вам получить доступ к тому, что в объекте и конструкторы прокладывают путь деструктору, принимая в качестве аргумента функцию, которую они применяют к частям объекта. Этот принцип может быть применен к другим типам.Суммы
Церковная кодировка дискриминируемого союза по сути двойственна церковной кодировке пары. Если пара состоит из двух частей, которые необходимо соединить, и вы можете выбрать извлечение одной или другой, вы можете создать объединение одним из двух способов, и при его использовании необходимо предусмотреть оба пути. Таким образом, есть два конструктора, и есть один деструктор, который принимает два аргумента.
Позвольте мне сократить тип
(a->t) -> (b->t) -> t
какSUM(a,b)(t)
. Тогда типы деструкторов и конструкторов:таким образом
Списки
Для списка примените снова тот же принцип. Список, элементы которого имеют тип,
a
может быть построен двумя способами: это может быть пустой список, или это может быть элемент (голова) плюс список (хвост). По сравнению с парами, есть немного поворот , когда дело доходит до деструкторов , вы не можете иметь два отдельных деструкторовhead
иtail
потому , что они будут работать только на непустых списках. Вам нужен один деструктор с двумя аргументами, один из которых является функцией с 0 аргументами (то есть значением) для случая nil, а другой - с 2 аргументами для случая cons. Функции, какis_empty
,head
иtail
могут быть получены из этого. Как и в случае сумм, список является непосредственно его собственной функцией-деструктором.Каждая из этих функций является полиморфной. Если вы работаете с типами этих функций, вы заметите, что это неT T1, … , ТN
cons
является одинаковым: тип результата не совпадает с типом аргумента. Тип результата - переменная - она более общая, чем аргумент. Если вы объединяетеcons
вызовы, последовательные вызовы для создания списка создаютсяcons
в разных типах. Это важно, чтобы списки работали в отсутствие рекурсивных типов. Таким способом вы можете создавать гетерогенные списки. На самом деле типы, которые вы можете выразить, не являются «спискомКак вы предполагаете, если вы хотите определить тип, который содержит только однородные списки, вам нужны рекурсивные типы. Почему? Давайте посмотрим на тип списка. Список кодируется как функция, которая принимает два аргумента: значение, возвращаемое в пустых списках, и функция, которая вычисляет значение, возвращаемое в ячейке cons. Позвольте
a
быть типом элемента,b
типом списка иc
типом, возвращаемым деструктором. Тип спискаСоздание списка однородным означает, что если это cons-ячейка, то хвост должен иметь тот же тип, что и целое, т. Е. Добавляет ограничение
Система типов Хиндли-Милнера может быть расширена такими рекурсивными типами, и на самом деле практические языки программирования делают это. Практические языки программирования, как правило, запрещают такие «голые» уравнения и требуют конструктора данных, но это не является внутренним требованием лежащей в основе теории. Требование конструктора данных упрощает вывод типов и на практике стремится избежать принятия функций, которые на самом деле содержат ошибки, но могут быть типизированными с некоторым непреднамеренным ограничением, которое вызывает трудную для понимания ошибку типа при использовании функции. Вот почему, например, OCaml принимает неохраняемые рекурсивные типы только с
-rectypes
опцией компилятора не по умолчанию . Вот определения выше в синтаксисе OCaml, вместе с определением типа для однородных списков с использованием нотации дляпсевдонимы рекурсивных типов :type_expression as 'a
означает, что типtype_expression
объединен с переменной'a
.Складки
Если взглянуть на это более широко, какова функция, которая представляет структуру данных?
В общих чертах, структура данных представлена как ее функция сгиба . Это общая концепция для структур данных: функция сгиба - это функция высшего порядка, которая пересекает структуру данных. Существует технический смысл, в котором сгиб универсален : все «общие» обходы структуры данных могут быть выражены через сгиб. То, что структура данных может быть представлена в виде ее функции сгиба, показывает это: все, что вам нужно знать о структуре данных, это как ее обойти, остальное - деталь реализации.
источник
t
и игнорировать аргумент, который должен приниматьa
иb
(который именно так(a and b) or t
и говорит). И кажется, что у меня были бы такие же проблемы с суммами. А также, без рекурсивных типов у меня не будет однородного списка. Итак, в нескольких словах, вы хотите сказать, что я должен добавить правила сумм, продуктов и рекурсивных типов, чтобы получить однородные списки?case (right y) f g → g y
в виду в конце вашего раздела сумм ?Вы можете представлять типы сумм в виде типов продуктов с тегами и значениями. В этом случае мы можем немного обмануть и использовать один тег для представления нуля или нет, имея второй тег, представляющий пару голова / хвост.
Мы определяем логические значения обычным способом:
Затем список представляет собой пару с первым элементом в качестве логического значения, а второй элемент в виде пары голова / хвост. Некоторые основные функции списка:
источник