Визначте список, використовуючи лише систему типу Hindley-Milner


10

Я працюю над невеликим компілятором обчислення лямбда, який має працюючу систему виводу типу Хіндлі-Мілнера, і тепер також підтримує рекурсивну, давайте (не у зв'язаному коді), що, на мою думку, повинно бути достатньо, щоб зробити Тьюрінг завершеним .

Проблема зараз полягає в тому, що я не маю уявлення, як скласти списки його підтримки чи вони вже підтримують їх, і мені просто потрібно знайти спосіб їх кодування. Я хотів би мати можливість їх визначити без необхідності додавати нові правила до системи типів.

Найпростіший спосіб я уявити список x- це щось, що є null(або порожній список), або пара, що містить і an, 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: Я щойно зрозумів, що мені також потрібні рекурсивні типи, яких у мене немає, щоб визначити список, як я хочу.


Чи можете ви бути трохи більш конкретними щодо того, що саме ви реалізували? Ви реалізували просто набране лямбда-числення (з рекурсивними визначеннями) та дали йому параметричні поліморфізми у стилі Гіндлі-Мілнера? Або ви реалізували поліморфне обчислення лямбда другого порядку?
Андрій Бауер

Можливо, я можу запитати простішим способом: якщо я беру OCaml або SML і обмежую його чистими лямбда-термінами та рекурсивними визначеннями, це те, про що ви говорите?
Андрій Бауер

@AndrejBauer: Я змінив питання. Я не впевнений у OCaml та SML, але я майже впевнений, якщо ви візьмете Haskell і обмежите його ламбда-термінами та рекурсивними даними верхнього рівня (наприклад let func = \x -> (func x)), ви отримаєте те, що у мене є.
Хуан

1
Щоб, можливо, покращити своє запитання, перегляньте цей мета-пост .
Juho

Відповіді:


13

Пари

Це кодування - це церковне кодування пар. Подібні методи можуть кодувати булеві числа, цілі числа, списки та інші структури даних.

x:a; y:bpair x y(a -> b -> t) -> t¬

(abt)t¬(¬a¬bt)t(ab¬t)t(ab)t
ab tpairt

pairє конструктором для парного типу firstі secondє деструкторами. (Це ті самі слова, які використовуються в об'єктно-орієнтованому програмуванні; тут слова мають значення, яке пов'язане з логічною інтерпретацією типів і термінів, про які я тут не вступатиму.) Інтуїтивно, деструктори дозволяють отримати доступ до того, що є в об'єкті і конструктори прокладають шлях деструктору, беручи за аргумент функцію, яку вони застосовують до частин об'єкта. Цей принцип можна застосувати і до інших типів.

Суми

Кодування Церквою дискримінованого союзу по суті є подвійним до церковного кодування пари. Якщо пара має дві частини, які потрібно скласти разом, і ви можете витягти ту чи іншу, ви можете створити союз будь-яким із двох способів, і коли ви використовуєте його, ви повинні дозволити обидва способи. Таким чином, є два конструктори, і є один деструктор, який бере два аргументи.

let case w = λf. λg. w f g           case : ((a->t) -> (b->t) -> t) -> (a->t) -> (b->t) -> t
  (* or simply let case w = w *)
let left x = λf. λg. f x             left : a -> ((a->t) -> (b->t) -> t)
let right y = λf. λg. g x            right : b -> ((a->t) -> (b->t) -> t)

Дозвольте скоротити тип (a->t) -> (b->t) -> tяк SUM(a,b)(t). Тоді типи деструкторів і конструкторів:

case : SUM(a,b)(t) -> (a->t) -> (b->t) -> t
left : a -> SUM(a,b)(t)
right : b -> SUM(a,b)(t)

Таким чином

case (left x) f g → f x
case (rightt y) f g → g y

Списки

Для списку застосуйте знову той же принцип. Список, елементи якого мають тип, aможна побудувати двома способами: це може бути порожній список, або він може бути елементом (головою) плюс списком (хвіст). Якщо порівнювати з парами, то деструктори мають невеликий поворот: ви не можете мати двох окремих деструкторів headі tailтому, що вони працюватимуть лише у не порожніх списках. Вам потрібен один деструктор з двома аргументами, один з яких - функція 0-аргументів (тобто значення) для нульового випадку, а інший - 2-аргументаційна функція для випадку мінусів. Такі функції , як is_empty, headі tailможуть бути виведені з цього. Як і у випадку сум, у списку є безпосередньо своя функція деструктора.

let nil = λn. λc. n
let cons h t = λn. λc. c h t
let is_empty l = l true (λh. λt. false) 
let head l default = l default (λh. λt. h)
let tail l default = l default (λh. λt. t)

consconsconsTT1,,Tn

Як ви вважаєте, якщо ви хочете визначити тип, який містить лише однорідні списки, вам потрібні рекурсивні типи. Чому? Давайте розглянемо тип списку. Список кодується як функція, яка бере два аргументи: значення для повернення в порожні списки та функція для обчислення значення для повернення в комірку з мінусів. Нехай aбуде тип елемента, bтип списку та cтип, повернений деструктором. Тип списку

a -> (a -> b -> c) -> c

Зробити список однорідним - це означає, що якщо це клітина "проти", хвіст повинен мати такий же тип, як і весь, тобто додає обмеження

a -> (a -> b -> c) -> c = b

Систему типу Hindley-Milner можна розширити за допомогою таких рекурсивних типів, і фактично це роблять практичні мови програмування. Практичні мови програмування, як правило, забороняють такі «голі» рівняння і вимагають конструктора даних, але це не є сутнісною вимогою основної теорії. Потрібен конструктор даних спрощує висновок типу, і на практиці, як правило, уникає прийняття функцій, які насправді є помилковими, але трапляються з можливістю набору з деяким ненавмисним обмеженням, яке спричиняє важко зрозумілу помилку типу, у якій використовується функція. Ось чому, наприклад, OCaml приймає незахищені рекурсивні типи лише з -rectypesопцією компілятора, що не використовується за замовчуванням . Ось наведені вище визначення в синтаксисі OCaml разом з визначенням типу для однорідних списків, використовуючи позначення дляпсевдонім рекурсивних типів : type_expression as 'aозначає, що тип type_expressionуніфікований зі змінною 'a.

# let nil = fun n c -> n;;
val nil : 'a -> 'b -> 'a = <fun>
# let cons h t = fun n c -> c h t;;
val cons : 'a -> 'b -> 'c -> ('a -> 'b -> 'd) -> 'd = <fun>
# let is_empty l = l true (fun h t -> false);;
val is_empty : (bool -> ('a -> 'b -> bool) -> 'c) -> 'c = <fun>
# let head l default = l default (fun h t -> h);;
val head : ('a -> ('b -> 'c -> 'b) -> 'd) -> 'a -> 'd = <fun>
# let tail l default = l default (fun h t -> t);;
val tail : ('a -> ('b -> 'c -> 'c) -> 'd) -> 'a -> 'd = <fun>
# type ('a, 'b, 'c) ulist = 'c -> ('a -> 'b -> 'c) -> 'c;;
type ('a, 'b, 'c) ulist = 'c -> ('a -> 'b -> 'c) -> 'c
# is_empty (cons 1 nil);;
- : bool = false
# head (cons 1 nil) 0;;
- : int = 1
# head (tail (cons 1 (cons 2.0 nil)) nil) 0.;;
- : float = 2.

(* -rectypes is required for what follows *)
# type ('a, 'b, 'c) rlist = 'c -> ('a -> 'b -> 'c) -> 'c as 'b;;
type ('a, 'b, 'c) rlist = 'b constraint 'b = 'c -> ('a -> 'b -> 'c) -> 'c
# let rcons = (cons : 'a -> ('a, 'b, 'c) rlist -> ('a, 'b, 'c) rlist);;
val rcons :
  'a ->
  ('a, 'c -> ('a -> 'b -> 'c) -> 'c as 'b, 'c) rlist -> ('a, 'b, 'c) rlist =
  <fun>
# head (rcons 1 (rcons 2 nil)) 0;;
- : int = 1
# tail (rcons 1 (rcons 2 nil)) nil;;
- : 'a -> (int -> 'a -> 'a) -> 'a as 'a = <fun>
# rcons 1 (rcons 2.0 nil);;
Error: This expression has type
         (float, 'b -> (float -> 'a -> 'b) -> 'b as 'a, 'b) rlist = 'a
       but an expression was expected of type
         (int, 'b -> (int -> 'c -> 'b) -> 'b as 'c, 'b) rlist = 'c

Складки

Розглядаючи це трохи більш загально, яка функція представляє структуру даних?

  • nn
  • (x,y)xy
  • ini(x)ix
  • [x1,,xn]

Загалом, структура даних представлена ​​як її складна функція. Це загальне поняття для структур даних: складна функція - це функція вищого порядку, яка обходить структуру даних. Існує технічний сенс, в якому складка є універсальною : усі "загальні" траверси структури даних можуть бути виражені у вигляді кратності. Про те, що структуру даних можна представити як її складну функцію, це свідчить: все, що вам потрібно знати про структуру даних, - це як її перетинати, решта - це деталь реалізації.


Ви згадуєте " кодування церкви " цілих чисел, пар, сум, але для списків ви даєте кодування Скотта . Я думаю, це може бути трохи заплутано для тих, хто не знайомий з кодуванням індуктивних типів.
Стефан Гіменез

Тому в основному тип моєї пари насправді не є типом продукту, оскільки функція з цим типом може просто повернутись tі проігнорувати аргумент, який повинен прийняти aі b(що саме так (a and b) or tговорить). І, схоже, у мене були б такі самі проблеми із сумами. А також без рекурсивних типів у мене не буде однорідного списку. Отже, декількома словами, ви говорите, що я повинен додати правила суми, продукту та рекурсивного типу, щоб отримати однорідні списки?
Хуан

Ви мали case (right y) f g → g yна увазі наприкінці розділу " Суми "?
Хуан

@ StéphaneGimenez я не зрозумів. Я не звик працювати над цими кодуваннями в типовому світі. Чи можете ви дати посилання на кодування Церкви проти кодування Скотта?
Жил "ТАК - перестань бути злим"

@JuanLuisSoldi Ви напевно чули, що "не існує проблеми, яку неможливо вирішити із додатковим рівнем непрямості". Кодування церкви кодують структури даних як функції, додаючи рівень виклику функції: структура даних стає функцією другого порядку, яку ви застосуєте до функції, щоб діяти на частини. Якщо ви хочете однорідного типу списку, вам доведеться мати справу з тим, що тип хвоста такий самий, як і тип у всьому списку. Я думаю, що це має включати форму рекурсії типу.
Жил 'ТАК - перестань бути злим'

2

Ви можете представити типи сум як типи продуктів з тегами та значеннями. У цьому випадку ми можемо трохи обдурити і використати один тег для представлення нуля чи ні, маючи другий тег, який представляє пару голова / хвіст.

Булели визначаємо звичайним чином:

true = λi.λe.i
false = λi.λe.e
if = λcond.λthen.λelse.(cond then else)

Потім у списку йде пара з першим елементом як булева, а другим - як голова / хвіст. Деякі основні функції списку:

isNull = λl.(first l)
null = pair false false     --The second element doesn't matter in this case
cons = λh.λt.(pair true (pair h t ))
head = λl.(fst (snd l))   --This is a partial function
tail = λl.(snd (snd l))   --This is a partial function  

map = λf.λl.(if (isNull l)
                 null 
                 (cons (f (head l)) (map f (tail l) ) ) 

Але це не дасть мені однорідного списку, чи правильно це?
Хуан
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.