Можно ли реализовать нетипизированное лямбда-исчисление только с использованием дженериков?

Рассмотрим следующую реализацию нетипизированного лямбда-исчисления:

pub enum Term {
    Var(usize), // a variable with a De Bruijn index
    Abs(Box<Term>), // an abstraction
    App(Box<Term>, Box<Term>) // an application
}

impl Term {
    ...
}

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

Я знаю об обычных аргументах в enums vs. traits; даже если перечисления - лучший выбор, я все равно хотел бы знать, возможно ли это.

Мои основные строительные блоки до сих пор более или менее следующие:

#[derive(Clone, PartialEq, Eq)]
pub struct Var(usize);

#[derive(Clone, PartialEq, Eq)]
pub struct Abs<T: Term>(T);

#[derive(Clone, PartialEq, Eq)]
pub struct App<T: Term, U: Term>(T, U);

pub trait Term: Clone {
    fn abs(self) -> Abs<Self> { Abs(self) }

    fn app<T: Term>(self, other: T) -> App<Self, T> { App(self, other) }

    fn update_free_variables(&mut self, added_depth: usize, own_depth: usize);

    fn _apply<T: Term>(&mut self, other: &mut T, depth: usize); // this is a helper function that enables term tree traversal for Abs<T>::apply

    fn is_reducible(&self, limit: usize, count: &usize) -> bool;

    fn beta_reduce(&mut self, order: Order, limit: usize, verbose: bool) -> usize;
}

impl Var {
    pub fn new(index: usize) -> Self {
        assert!(index > 0);
        Var(index)
    }
}

impl<T: Term> Abs<T> {
    fn unabs(self) -> T {
        self.0
    }

    fn apply<U: Term>(mut self, other: &mut U) -> T {
        self._apply(other, 0);
        self.unabs()
    }
}

impl<T: Term, U: Term> App<T, U> {
    fn unapp(self) -> (T, U) {
        (self.0, self.1)
    }
}

// and some impl Term for X

Хотя реализовать базовые функции довольно легко, есть несколько моментов, в которых я изо всех сил пытаюсь найти правильное решение. Мне нужно уметь делать следующее:

  • создать синтаксический анализатор, способный интерпретировать любой термин, от простых переменных до сложных терминов, с помощью одной функции
  • заменить любую переменную (независимо от того, насколько глубоко она вложена) другой переменной или другим термином
  • рекурсивно сокращать термины (я не уверен, что это возможно с проанализированным термином, который, вероятно, должен быть трейт-объектом)

Я бы предпочел попытаться реализовать это самостоятельно, мне просто нужен совет относительно направления. Возможно ли это вообще без оболочки enum? Если да, то какой подход я должен использовать (с точки зрения безопасности объекта, unsafe обмана и т. д.)?


person ljedrz    schedule 25.10.2017    source источник
comment
Я знаю, что на данный момент код не является объектно-безопасным, но я не уверен, что это поможет моему делу (и мне нужно настроить много кода). Это просто ссылка, показывающая общую идею.   -  person ljedrz    schedule 25.10.2017


Ответы (1)


Перечисления против признаков

Различные термины должны иметь разные наборы методов, например. только абстракции должны быть «неабстрагируемыми», и только приложения должны быть оценены.

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

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

Одним из потенциальных преимуществ дизайна, основанного на признаках, является расширяемость, но в данной ситуации это не проблема, поскольку определение нетипизированного лямбда-исчисления фиксировано.

Три проблемы

создать синтаксический анализатор, способный интерпретировать любой термин, от простых переменных до сложных терминов, с помощью одной функции

Разбор выражений должен быть достаточно простым, если все приложения должны быть заключены в круглые скобки. У меня нет большого опыта синтаксического анализа, но я бы, вероятно, попробовал такой рекурсивный подход:

To read one term, given a mutable reference to a variable stack (which begins empty):
    If the next character is an opening parenthesis:
        Consume it.
        Read a term.
        Read a term.
        Make sure the next character is a closing parenthesis, and consume it.
        Return an application of the two terms.
    If the next character is a lambda:
        Consume it.
        Make sure the next character is a variable, then consume it.
        Make sure the next character is a dot, and consume it.
        Push the variable to the variable stack.
        Read a term.
        Pop the variable off of the stack.
        Return an abstraction of the term.
    If the next character is a variable:
        Consume it.
        Search the variable stack find the first index of the variable from the top.
        Return a variable term with this index.

Вы можете изменить это, чтобы принять общие сокращения в нотации лямбда-исчисления, такие как (a b c) для ((a b) c). В настоящее время он принимает λx.λy.λz.((x z) (y z)), но не λx.λy.λz.x z (y z).

заменить любую переменную (независимо от того, насколько глубоко она вложена) другой переменной или другим термином

Я предполагаю, что число, хранимое переменным термином, — это количество слоев абстракции между точкой, где переменная вводится, и точкой, где она используется. Если это так, то вы можете рекурсивно обходить структуру, запоминая при этом текущую глубину абстракции. Когда встречается переменная, соответствующая номеру, она заменяется данным термином, за исключением того, что все свободные переменные в терме, которые можно найти, ища переменные с числом, большим, чем их глубина абстракции в данном терме, к их числу должна быть добавлена ​​текущая глубина абстракции, чтобы учесть разницу в уровнях. Когда приложение встречается, процесс замены рекурсивно выполняется в обоих его дочерних элементах. Когда встречается новая абстракция, подстановка выполняется рекурсивно в ее теле, при этом глубина абстракции увеличивается на 1 для учета нового уровня.

Если вы действительно хотите использовать трейты, то Term будет иметь такой метод:

fn substitute(&mut self, variable_number: usize, other: &Term);

Просто чтобы уточнить систему нумерации, правильно ли следующее?

λn.λf.λx.(f ((n f) x))Abs(Abs(Abs(App(Var(1),App(App(Var(2),Var(1)),Var(0))))))

рекурсивно сокращать термины (я не уверен, что это возможно с проанализированным термином, который, вероятно, должен быть трейт-объектом)

Хотя это громоздко, это возможно с трейт-объектами. Вы можете определить два метода в Term, первый из которых ничего не сделает, кроме как в реализации Abs, где он берет терм и возвращает тело абстракции, подставляя данный терм в переменную с индексом 0. Второй метод ничего не делает, кроме в реализации App, где он будет вызывать первый метод в левом термине приложения, передавая правый термин. Используя аналогичную стратегию, вы можете определить методы для поиска бета-приводимых терминов, и, комбинируя эти методы, вы получите беспорядочную эмуляцию сопоставления с образцом перечисления, которая будет гораздо лучшим инструментом для этой задачи.

Этот документ может оказаться полезным для вас при реализации бета-сокращения. Я мало что читал, но, похоже, он предлагает стратегии для эффективных алгоритмов бета-редукции.

Enums против трейтов снова

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

person bearbear2k    schedule 27.10.2017
comment
Я обновил свой вопрос - я буду рад отшлифовать его дальше, если что-то все еще неясно (например, я слишком усложнил бета-сокращение, сославшись только на один из его строительных блоков). Что касается системы нумерации, вы правы - я должен был упомянуть, что использую индексы Де Брюйна (я думаю, что это самый простой способ реализации). - person ljedrz; 27.10.2017
comment
Я преобразовал большую часть базового кода в трейт-объекты, но столкнулся с препятствием: функция substitute, в которой я использую mem::swap, жалуется, что Term does not have a constant size known at compile-time. Я мог бы сделать это возможным, объявив trait Term: Sized, но это нарушило бы безопасность объекта. Есть ли способ обойти это? - person ljedrz; 31.10.2017
comment
Нет смысла использовать mem::swap для Term типовых объектов, потому что разные типы терминов имеют разные представления в памяти. С какой целью вы пытаетесь его использовать? - person bearbear2k; 09.11.2017
comment
Правильно - проблема в том, что я не знаю, как это обойти; Я хотел выполнить замену переменной. - person ljedrz; 09.11.2017