Простейший пример необходимости унификации при выводе типов

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

Я приведу пример на «псевдо-C #», чтобы прояснить ситуацию:

Наивный способ сделать это будет примерно так:

Предположим, вы «разбираете» свою программу в дерево выражений, чтобы ее можно было выполнить с помощью:

interface IEnvironment
{
    object lookup(string name);
}

interface IExpression
{
    // Evaluate this program in this environment
    object Evaluate(IEnvironment e);
}

Так что что-то вроде "Умножения" может быть реализовано с помощью:

class Multiply : IExpression
{
    IExpression lhs;
    IExpression rhs;
    // etc.
    public object Evaluate(IEnvironment e)
    {
        // assume for the moment C# has polymorphic multiplication
        return lhs.Evaluate(e) * rhs.Evaluate(e);
    }
}

Затем, чтобы «реализовать» вывод типа, вы могли бы просто сделать что-то вроде:

interface ITypeEnvironment
{
    Type getType(string name);
}

interface IExpression
{
    //as before
    object Evaluate(IEnvironment e);
    // infer type
    Type inferType(ITypeEnvironment typeEnvironment);
}

Тогда вывод типа "Умножения" может выглядеть примерно так:

class Multiply : IExpression
{
    IExpression lhs;
    IExpression rhs;

    // ... 
    public Type inferType(ITypeEnvironment typeEnvironment)
    {
        Type lhsType = lhs.inferType(typeEnvironment);
        Type rhsType = rhs.inferType(typeEnvironment);
        if(lhsType != rhsType)
             throw new Exception("lhs and rhs types do not match");

        // you could also check here that lhs/rhs are one of double/int/float etc.
        return lhsType;
    }
}

lhs и rhs могут быть простыми константами или «переменными», которые ищутся в среде:

class Constant : IExpression
{
    object value;
    public Type inferType(ITypeEnvironment typeEnvironment)
    {
        return value.GetType(); // The type of the value;
    }
    public object Evaluate(IEnvironment environment)
    {
        return value;
    }
}

class Variable : IExpression
{
    string name;
    public Type inferType(ITypeEnvironment typeEnvironment)
    {
        return typeEnvironment.getType(name);
    }
    public object Evaluate(IEnvironment environment)
    {
        return environment.lookup(name);
    }
}

Но на этом нигде не заканчивается необходимость в алгоритме «унификации».

Итак, очевидно, что мой пример недостаточно сложен. Нужны ли ему функции высшего порядка? Нужен ли нам «параметрический полиморфизм»?

Каков самый простой пример, когда "объединение" действительно необходимо для правильного определения типа выражения.

Пример на Scheme был бы идеальным (т.е.пример очень маленькой программы Scheme, где вам требуется унификация, чтобы правильно вывести тип s-выражения).


person Paul Hollingsworth    schedule 15.07.2009    source источник
comment
Прежде чем я отвечу, недостаточно ли генериков C #? Они обеспечивают вывод типа во время компиляции.   -  person Dykam    schedule 15.07.2009
comment
Пример схемы был бы идеальным. Как и в размеченном примере, я думаю, что просто вспомогательные функции достаточно сложны: (lambda (x) (+ x 5)) как выражение, не позволяет мне просто спросить у среды типов тип x. Таким образом, любая функция языка с ограничениями привязки вызывает проблему.   -  person Paul Hollingsworth    schedule 16.07.2009


Ответы (3)


public Type inferType(ITypeEnvironment typeEnvironment)
{
    return typeEnvironment.getType(name);
}

Что делать, если вы просто не знаете тип переменной? В этом весь смысл вывода типов, верно? Что-то очень простое (на каком-то языке псевдокода):

function foo(x) { return x + 5; }

Вы не знаете тип x, пока не сделаете вывод о сложении и не поймете, что это должно быть целое число, потому что оно добавлено к целому числу или что-то в этом роде.

Что делать, если у вас есть еще одна такая функция:

function bar(x) { return foo(x); }

Вы не можете определить тип x, пока не выясните тип foo, и так далее.

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

person newacct    schedule 15.07.2009
comment
А я вижу - функций достаточно. Вы не знаете тип x, но тогда x + 5 означает, что x должно быть 5 ... - person Paul Hollingsworth; 16.07.2009
comment
подразумевает, что x должен быть int, что означает - person Paul Hollingsworth; 16.07.2009
comment
Не совсем - это просто означает, что x должен быть чем-то, для чего существует бинарный оператор +, для которого rhs является int (или чем-то, для чего существует неявное преобразование из int, если это есть в языке). Обладая такой большой гибкостью, вы можете просто определить bar как общий метод bar<T>(T x), где T имеет ограничение, что он должен поддерживать оператор + с целым числом (компилятор определяет ограничение, исходя из того, как вы использовали его внутри bar). Так что нет необходимости привязывать тип x при компиляции bar. - person Daniel Earwicker; 23.07.2009

Позвольте мне полностью проигнорировать ваш пример и дать вам пример того, где мы делаем вывод типа метода в C #. (Если эта тема вас интересует, я рекомендую вам прочитать "типа вывода "моего блога.)

Учитывать:

void M<T>(IDictionary<string, List<T>> x) {}

Здесь у нас есть общий метод M, который берет словарь, который отображает строки в списки T. Предположим, у вас есть переменная:

var d = new Dictionary<string, List<int>>() { ...initializers here... };
M(d);

Мы вызвали M<T> без аргумента типа, поэтому компилятор должен вывести его.

Компилятор делает это путем «объединения» Dictionary<string, List<int>> с IDictionary<string, List<T>>.

Сначала он определяет, что Dictionary<K, V> реализует IDictionary<K, V>.

Из этого мы делаем вывод, что Dictionary<string, List<int>> реализует IDictionary<string, List<int>>.

Теперь у нас есть совпадение по IDictionary части. Мы объединяем строку со строкой, что, конечно, хорошо, но мы ничего из этого не узнаем.

Затем мы объединяем List со List и понимаем, что нам снова нужно выполнять рекурсию.

Затем мы объединяем int с T и понимаем, что int является границей T.

Алгоритм вывода типов продолжает работать до тех пор, пока он не перестает прогрессировать, а затем начинает делать дальнейшие выводы из своих выводов. Единственное ограничение на T - это int, поэтому мы делаем вывод, что вызывающий должен хотел, чтобы T был int. Итак, мы звоним M<int>.

Это ясно?

person Eric Lippert    schedule 15.07.2009
comment
Да, я уже подписан на ваш блог - рад видеть вас на Stack Overflow! Итак, я полагаю, что унификация происходит из композиции типов из других типов ... так что, возможно, функций более высокого порядка достаточно в схемном языке, чтобы обеспечить необходимость унификации ... хм ... - person Paul Hollingsworth; 16.07.2009
comment
Чтобы дать больше контекста - написал предметно-ориентированный язык, на котором я работаю, чей интерпретатор написан на C # ... мы ищем возможность добавить к нему вывод типа, чтобы обеспечить лучшую предварительную проверку. На самом деле, это не совсем предметная область - это, по сути, интерпретатор схемы, который очень хорошо интегрирован с нашим приложением. Мы уже добавили полную поддержку продолжений, поэтому мы подумали, что тоже будет неплохо какой-то вывод типов! Но сначала мы должны понять, как это сделать! - person Paul Hollingsworth; 16.07.2009
comment
О, и пока вы, возможно, читаете это: единственной самой большой победой для C # была бы лучшая поддержка для обнаружения ошибок использования IDisposable. Я бы подпрыгивал и радовался от радости, если бы вы это добавили! - person Paul Hollingsworth; 16.07.2009

Предположим, у нас есть функция

f(x, y)

где x может быть, например, FunctionOfTwoFunctionsOfInteger или это может быть FunctionOfInteger.

Предположим, мы проходим

f(g(u, 2), g(1, z))

Теперь с объединением u привязан к 1, а z привязан к 2, поэтому первым аргументом является FunctionOfTwoFunctionsOfInteger.

Итак, мы должны сделать вывод, что оба типа x и y - это FunctionOfTwoFunctionsOfInteger

Я не слишком знаком с C #, но с лямбда-выражениями (или эквивалентными делегатами или чем-то еще) это должно быть возможно.

В качестве примера того, где вывод типа очень полезен для повышения скорости доказательства теорем, взгляните на "Шуберта Steamroller"

http://www.rpi.edu/~brings/PAI/schub.steam/node1.html

Существует выпуск Journal of Automated Reasoning, посвященный решениям и формулировкам этой проблемы, большинство из которых связано с типизированным выводом в системах доказательства теорем:

http://www.springerlink.com/content/k1425x112w6671g0/

person Larry Watanabe    schedule 15.07.2009
comment
Я не совсем понимаю это ... Мне нужен простой пример! ;-) Но спасибо за ссылки посмотрю - person Paul Hollingsworth; 16.07.2009