Представлення зв'язаних змінних з функцією від використання до зв'язуючих


11

Проблема представлення зв'язаних змінних у синтаксисі, зокрема, заміщення, що уникає захоплення, є загальновідомою та має ряд рішень: названі змінні з альфа-еквівалентністю, індекси de Bruijn, локальні безіменності, номінальні множини тощо.

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

λx.(λy.xy)

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

Очевидно, що це не є розумним для написання синтаксису на сторінці для того, щоб люди читали, але тоді вони також не є індексами де Бруйна. Мені здається, що це має ідеальний сенс математично, і, зокрема, робить підстановку, що уникає захоплення, дуже просто: просто опустіть термін, який ви замінюєте, і прийміть об'єднання функцій зв'язування. Це правда, що він не має поняття "вільна змінна", але тоді (знову ж таки) також не мають індексів де Бреййн насправді; в будь-якому випадку термін, що містить вільні змінні, представляється терміном зі списком "контекстних" в'яжучих спереду.

Я щось пропускаю, і є причина, чому це представлення не працює? Чи є проблеми, які роблять це набагато гірше, ніж інші, про що не варто замислюватися? (Єдиною проблемою, про яку я зараз можу подумати, є те, що набір термінів (разом із їх обов'язковими функціями) не визначений індуктивно, але це не здається нездоланним.) Або дійсно є місця, де він був використаний?


2
Я не знаю про недоліки. Можливо, формалізація (наприклад, у кореспондента-кореспондента) важча? Я не впевнений ... Що я знаю, це те, що технічно немає нічого поганого: такий спосіб бачити лямбда-терміни пропонується їх представленням як доказових мереж, тому люди, що знають, що знають доказ (як я), явно використовують це весь час. Але люди, що знають про те, як відомо, дуже рідкісні :-) Тож, можливо, це справді питання традиції. PS: Я додав пару слабко пов'язаних тегів, щоб зробити питання більш помітним (сподіваємось).
Даміано Мацца

Чи не такий підхід є еквівалентним абстрактному синтаксису вищого порядку (тобто, що представляє зв'язувачі як функції на хост-мові)? У певному сенсі використання функції в якості в'яжучого встановлює покажчики на зв'язувачі неявно, у поданні закриттів.
Родольф Лепігр

2
@RodolpheLepigre Я так не думаю. Зокрема, я розумію, що HOAS правильний лише тоді, коли метатеорія є досить слабкою, тоді як такий підхід є правильним у довільній метатеорії.
Майк Шульман

3
Правильно, тому кожне в'яжуче використовує унікальну (в межах дерева) назву змінної (вказівник на неї автоматично один). Це конвенція Барендрегта. Але замінивши, ви повинні відновити (на C) те, що заміняєте, і надалі мати унікальні імена. В іншому випадку (загалом) ви використовуєте однакові покажчики для декількох підрядів, і ви можете отримати змінне захоплення. Перебудова - альфа-перейменування. Імовірно, щось подібне відбувається залежно від специфіки кодування дерев як наборів?
Dan Doel

3
@DanDoel Ах, цікаво. Я думав, що це настільки очевидно, що не потрібно згадувати, що ви потрапляєте в окрему копію терміна, що підміняється при кожному виникненні змінної, на яку він підміняється; інакше у вас більше не було б синтаксичного дерева ! Мені не прийшло в голову сприймати це копіювання як альфа-перейменування, але тепер, коли ви його вказали, я можу це побачити.
Майк Шульман

Відповіді:


11

Відповіді Андрія та Лукаша дають хороші моменти, але я хотів додати додаткові коментарі.

Для повторення того, що сказав Даміано, такий спосіб представлення зв'язування за допомогою покажчиків - це той спосіб, який запропонував доказів-мереж, але найдавніше місце, де я бачив це за лямбда-термінами, було у старому нарисі Кнута:

  • Дональд Кнут (1970). Приклади формальної семантики. У симпозіумі з семантики алгоритмічних мов Е. Енгелер (ред.), Записки лекції з математики 188, Спрингер.

На сторінці 234 він намалював таку схему (яку він назвав "інформаційною структурою"), що представляє термін :(λу.λz.уz)х

Діаграма Кнута для $ (\ lambda y. \ Lambda z.yz) x $

Цей вид графічного зображення ламбда-термінів також вивчався незалежно (і більш глибоко) у двох тезах на початку 1970-х років, як Крістофер Вадсворт (1971, « Семантика і прагматика лямбда-числення» ), так і Річард Статман (1974, « Структурна складність»). доказів ). В даний час такі діаграми часто називають "λ-графіками" (див., Наприклад, цей документ ).

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

α


10

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

З вашого коментаря до відповіді Андрія я роблю висновок, що ви певною мірою зацікавлені поділитися. Я можу надати тут деякий вклад.

У типовому набраному лямбдальному обчисленні ослаблення та скорочення, всупереч іншим правилам, не мають синтаксису.

Γт:ТΓ,х:Ат:ТW
Γ,х1:А,х2:Ат:ТΓ,х:Ат:ТС

Додамо трохи синтаксису:

Γт:ТΓ,х:АWх(т):ТW
Γ,х1:А,х2:Ат:ТΓ,х:АСхх1,х2(т):ТС

Саб,c()аб,c

З цим синтаксисом кожна змінна використовується рівно двічі, один раз там, де вона пов'язана, і один раз, де вона використовується. Це дозволяє нам дистанціюватися від певного синтаксису і розглядати термін як графік, де змінні та терміни є ребрами.

З алгоритмічної складності тепер ми можемо використовувати покажчики не від змінної до сполучної, а від сполучної до змінної і мати підстановки в постійному часі.

Більше того, це переформулювання дозволяє нам відслідковувати стирання, копіювання та обмін із більшою достовірністю. Можна написати правила, які поступово копіюють (або стирають) термін під час спільного використання підрозділів. Є багато способів зробити це. У деяких обмежених налаштуваннях виграші досить дивовижні .

Це наближається до тем мереж взаємодії, комбінаторів взаємодії, явної підстановки, лінійної логіки, оптимальної оцінки Лампінга, обміну графіками, легкої логіки та іншого.

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


6

Ваша структура даних працює, але вона не буде більш ефективною, ніж інші підходи, тому що вам потрібно копіювати кожен аргумент при кожному зменшенні бета-версії, і ви повинні зробити стільки копій, скільки є випадків пов'язаної змінної. Таким чином ви продовжуєте руйнувати обмін пам’яттю між підтермінами. У поєднанні з тим, що ви пропонуєте нечисте рішення, яке передбачає маніпуляції з покажчиком, і тому дуже схильне до помилок, це, мабуть, не варто турбуватися.

Але я був би радий подивитися експеримент! Ви можете взяти lambdaта реалізувати її зі своєю структурою даних (OCaml має покажчики, їх називають посиланнями ). Більше чи менше, вам просто доведеться замінити syntax.mlі norm.mlсвоїми версіями. Це менше 150 рядків коду.


Дякую! Признаюсь, я насправді не дуже задумувався над реалізаціями, але в основному про те, щоб вміти робити математичні докази, не турбуючись ні про ведення бухгалтерії де Бруйна, ні про перейменування альфами. Але чи є ймовірність, що реалізація може зберегти деякий обмін пам’яттю, не роблячи копій «до необхідності», тобто поки копії не розходяться одна від одної?
Майк Шульман

β(λх.е1)е2е1е2

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

2
Додамо, я працював над реалізацією, яка в основному використовувала цю техніку (але з унікальними цілими числами і картами, а не покажчиками), і я не дуже рекомендував би її. У нас, безумовно, було багато помилок, де ми пропустили клонувати речі належним чином (ні в чому невеликій частині через намагання уникнути цього, коли це можливо). Але я думаю, що там є документ деяких людей з GHC, де вони виступають за це (я вважаю, що вони використовували хеш-функцію для створення унікальних імен). Це може залежати, що саме ти робиш. У моєму випадку це було висновок / перевірка, і, здається, він там дуже погано підходить.
Dan Doel

@MikeShulman Для алгоритмів розумної (Елементарної) складності (значною мірою кількість копіювання та стирання) так звана "абстрактна частина" оптимального скорочення Лампінга не робить копії, поки це не потрібно. Абстракційна частина також є суперечливою частиною на відміну від повного алгоритму, який вимагає деяких анотацій, які можуть домінувати в обчисленні.
Łukasz Lew

5

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

Коли ви говорите "функція, яка відображає кожну змінну до в'яжучої речовини, в області якої вона лежить": тип виводу цієї функції, безумовно, трохи тонший, ніж це звучить! Зокрема, функція повинна приймати значення приблизно як "зв'язувачі розглянутого терміна" - тобто деякий набір, який змінюється залежно від терміна (і, очевидно, не є підмножиною більшого набору оточуючих будь-яким корисним способом). Отже, підміняючи, ви не можете просто "прийняти об'єднання функцій зв'язування": вам також доведеться перенастроювати їх значення, згідно з деякою картою від сполучників у початкових термінах до в'яжучих в результаті заміни.

Ці перепланування, безумовно, повинні бути "звичайними", в тому сенсі, що вони можуть бути розумно або змітані під килим, або добре упаковані з точки зору якоїсь функціональності чи природності. Але те саме стосується бухгалтерського обліку, що бере участь у роботі з названими змінними. В цілому, мені здається, ймовірно, що з цим підходом було б принаймні стільки бухгалтерського обліку, скільки з більш стандартними підходами.

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


відстеження сфери застосування кожної змінної дійсно вимагає ведення бухгалтерського обліку, але не слід підходити до висновку, що завжди потрібно обмежуватися чітко визначеним синтаксисом! Такі операції, як заміна та скорочення бета-версії, можна визначити навіть за умови неправильного застосування, і я підозрюю, що якби хтось хотів формалізувати цей підхід (що знову ж таки, це насправді підхід мереж / «λ-графіків») у Довідник-помічник, спершу слід було б реалізувати більш загальні операції, а потім довести, що вони зберігають властивість доброго контролю.
Ноам Зейльбергер

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


5

λLazy.t

В цілому, я вважаю, що це круте представлення, але воно передбачає деяку бухгалтерію з покажчиками, щоб уникнути розриву обов'язкових зв’язків. Можна було б змінити код на використання змінних полів, я думаю, але кодування в Coq було б менш прямим. Я все ще переконаний, що це дуже схоже на HOAS, хоча структура вказівника робиться явною. Однак, присутність Lazy.tозначає, що деякий код можна оцінити не в той час. У моєму коді це не так, оскільки forceвчасно може відбуватися лише заміна змінної змінною (а не оцінка, наприклад).

(* Representation of a term of the λ-calculus. *)
type term =
  | FVar of string      (* Free variable  *)
  | BVar of bvar        (* Bound variable *)
  | Appl of term * term (* Application    *)
  | Abst of abst        (* Abstraction    *)

(* A bound variable is a pointer to the corresponding binder. *)
and bvar = abst

(* A binder is represented as its body in which the bound variable points to
   the binder itself. Note that we need to use a thunk to be able to work
   underneath a binder (for substitution, evaluation, ...). A name can be
   given for easy printing, but no renaming is done. Only “visual capture”
   can happen since pointers are established the right way, even if names
   can clash. *)
and abst = { body : term Lazy.t ; name : string }

(* Terms can be built with recursive values for abstractions. *)

(* Krivine's notation is used for application (function in parentheses). *)

let id    : term = (* λx.x        *)
  Abst(let rec id = {body = lazy (BVar(id)); name = "x"} in id)

let idid  : term = (* (λx.x) λx.x *)
  Appl(id, id)

let delta : term = (* λx.(x) x *)
  Abst(let rec d = {body = lazy (Appl(BVar(d), BVar(d))); name = "x" } in d)

let weird : term = (* (λx.x) λy.(λx.(x) x) (C) y *)
  Appl(id, Abst(let rec x = {body = lazy (Appl(delta, Appl(FVar("C"),
    BVar(x)))); name = "y"} in x))

let omega : term = (* (λx.(x) x) λx.(x) x *)
  Appl(delta, delta)

(* Printing function is immediate. *)
let rec print : out_channel -> term -> unit = fun oc t ->
  match t with
  | FVar(x)   -> output_string oc x
  | BVar(x)   -> output_string oc x.name
  | Appl(t,u) -> Printf.fprintf oc "(%a) %a" print t print u
  | Abst(f)   -> Printf.fprintf oc "λ%s.%a" f.name print (Lazy.force f.body)

(* Substitution of variable [x] by [v] in the term [t]. Occurences of [x] in
   [t] are identified using physical equality ([BVar] case). The subtle case
   is [Abst], because we need to reestablish the physical link between the
   binder and the variable it binds. *)
let rec subst_var : bvar -> term -> term -> term = fun x t v ->
  match t with
  | FVar(_)   -> t
  | BVar(y)   -> if y == x then v else t
  | Appl(t,u) -> Appl(subst_var x t v, subst_var x u v)
  | Abst(f)   ->
      (* First compute the new body. *)
      let fv = subst_var x (Lazy.force f.body) v in
      (* Reestablish the physical link, using [subst_var] itself again. This
         requires a second traversal of the term. We could probably do both
         at once, but who cares the complexity is linear in [t] anyway. *)
      Abst(let rec g = {f with body = lazy (subst_var f fv (BVar(g)))} in g)

(* Actual substitution function. *)
let subst : abst -> term -> term = fun f v ->
  subst_var f (Lazy.force f.body) v

(* Normalization function (all the way, even under binders). *)
let rec eval : term -> term = fun t ->
  match t with
  | Appl(t,u) ->
      begin
        let v = eval u in
        match eval t with
        | Abst(f) -> eval (subst f v)
        | t       -> Appl(t,v)
      end
  | Abst(f)   ->
      (* Actual computation in the body. *)
      let fv = eval (Lazy.force f.body) in
      (* Here, the physical link is reestablished, but it is important to note
         that the computation of evaluation is done above. So the part below
         only takes a linear time in the size of the normal form of the body
         of the abstraction. *)
      Abst(let rec g = {f with body = lazy (subst_var f fv (BVar(g)))} in g)
  | _         ->
      t

let _ = Printf.printf "id         = %a\n%!" print id
let _ = Printf.printf "eval id    = %a\n%!" print (eval id)

let _ = Printf.printf "idid       = %a\n%!" print idid
let _ = Printf.printf "eval idid  = %a\n%!" print (eval idid)

let _ = Printf.printf "delta      = %a\n%!" print delta
let _ = Printf.printf "eval delta = %a\n%!" print (eval delta)

let _ = Printf.printf "omega      = %a\n%!" print omega
(* The following obviously loops. *)
(*let _ = Printf.printf "eval omega = %a\n%!" print (eval omega)*)

let _ = Printf.printf "weird      = %a\n%!" print weird
let _ = Printf.printf "eval weird = %a\n%!" print (eval weird)

(* Output produced:
id         = λx.x
eval id    = λx.x
idid       = (λx.x) λx.x
eval idid  = λx.x
delta      = λx.(x) x
eval delta = λx.(x) x
omega      = (λx.(x) x) λx.(x) x
weird      = (λx.x) λy.(λx.(x) x) (C) y
eval weird = λy.((C) y) (C) y
*)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.