Определение альтернативного Y комбинатора - PullRequest
9 голосов
/ 21 января 2011

В последнее время я провел некоторое время, оборачиваясь вокруг комбинатора Y, и обнаружил, что он обычно определяется (более или менее) следующим образом (это в C #, но язык выбора не важен):

public delegate TResult SelfApplicable<TResult>(SelfApplicable<TResult> r);

public static TResult U<TResult>(SelfApplicable<TResult> r)
{
    return r(r);
}

public static Func<TArg1, TReturn> Y<TArg1, TReturn>(Func<Func<TArg1, TReturn>, Func<TArg1, TReturn>> f)
{
    return U<Func<TArg1, TReturn>>(r => arg1 => f(U(r))(arg1));
}

Хотя это совершенно функционально (каламбур), мне кажется, что мое определение намного проще:

public static Func<TArg1, TReturn> Y<TArg1, TReturn>(Func<Func<TArg1, TReturn>, Func<TArg1, TReturn>> f)
{
    return f(n => Y(f)(n));
}

Есть ли причина, по которой последнее определение не столь распространено (я еще не нашел его в сети)?Может быть, это как-то связано с определением Y в терминах самого себя?

Ответы [ 3 ]

5 голосов
/ 22 января 2011

Анонимная рекурсия, то есть с комбинатором с фиксированной запятой, не часто встречается в императивных, строго типизированных языках, по той простой причине, что проще назвать вашу [цензурированную] функцию, чем определить анонимную функцию, которая выполняетта же задача.Кроме того, OOA & D учит нас, что код, который имеет значение в нескольких местах, не должен дублироваться;он должен быть назван и таким образом доступен из общего места.Лямбды по своей природе одноразовые;способ указания нескольких строк очень специфичного для конкретной ситуации кода для использования в более общем алгоритме, таком как циклические конструкции.Большинство рекурсивных алгоритмов - это вещи, которые имеют довольно общее применение (сортировка, генерация рекурсивных рядов и т. Д.), Что обычно приводит к тому, что вы делаете их более доступными.

Помимо лямбда-исчисления, в большинстве языков программирования анонимная функция F должнасуществовать, прежде чем его можно будет использовать.Это исключает определение функции в терминах самой себя.В некоторых функциональных языках, таких как Erlang, функция F определяется с использованием «перегрузок», где более простые функции используются в качестве базовых вариантов для более сложных:

Fact(0) -> 1

Fact(i) -> Fact(i-1) * i

Это было бы хорошо, за исключением того, что в Erlang-В настоящее время это именованная функция «Факт», и при вызове этого метода программа будет «падать» через перегрузки, пока не найдет первую, для которой соответствуют параметры.В C # нет эквивалента этой точной конструкции, потому что C # не поддерживает выбор перегрузки на основе значения.

Хитрость заключается в том, чтобы каким-то образом получить ссылку на функцию, которую можно передать функции.Есть много способов, каждый из которых требует уже существующей ссылки.Если вы не можете обратиться к функции по имени, тогда тип функции FP-комбинатора - Func<Func<Func<Func<Func<....Метод Конрада является самым простым, но в C # он заканчивается своего рода хаком (он компилируется, но ReSharper все еще жалуется, что это возможное InvalidOperationException; не может вызвать нулевой указатель метода).

Вот что я использую для простых случаев, в основном использую обходной путь делегата для неспособности неявно ввести неявно типизированную лямбду:

public static class YCombinator
{
    public delegate TOut RLambda<TIn, TOut>(RLambda<TIn, TOut> rLambda, TIn a);
    public static Func<T,T> Curry<T>(this RLambda<T,T> rLambda)
    {
        return a => rLambda(rLambda, a);
    }
}

//usage
var curriedLambda = YCombinator.Curry<int>((f, i) => i <= 0 ? 1 : f(f, i - 1)*i)
var shouldBe120 = curriedLambda(5);

Вы можете объявить перегрузку Curry<TIn, TOut> дляобрабатывать случаи, когда тип ввода не является типом вывода, например, создание списка первых N простых чисел;эта функция P может быть рекурсивно определена как функция, которая создает список всех натуральных чисел, которые не делятся ни на одно меньшее простое число.Фиксированная точка P (1) => 2 определяет базовый случай, из которого может быть определен рекурсивный алгоритм (хотя и не очень эффективный):

var curriedLambda =
            YCombinator.Curry<int, List<int>>(
                (p, i) => i == 1
                              ? new List<int>{2}
                              : p(p, i - 1)
                                    .Concat(new[]
                                                {
                                                    Enumerable.Range(p(p, i - 1)[i - 2],
                                                                     int.MaxValue - p(p, i - 1)[i - 2])
                                                        .First(x => p(p, i - 1).All(y => x%y != 0))
                                                }).ToList()
                );
        Assert.AreEqual(new []{2,3,5,7,11}, curriedLambda(5));

И, таким образом, головоломка представляет себя;хотя вы, безусловно, МОЖЕТЕ определить все как функцию более высокого порядка, этот первичный искатель будет НАМНОГО быстрее, если будет определен императивно, а не функционально.Основное ускорение - это просто определение p (p, i-1) на каждом уровне, поэтому оно не оценивается 3 раза за уровень рекурсии.Умный язык, разработанный для функциональной работы, сделает это за вас.

3 голосов
/ 22 января 2011

Я не уверен, что ваш вопрос, но я думаю, причина, по которой ни Y-комбинатор, ни ваше решение не встречаются в дикой природе, двояка:

  1. анонимные рекурсивные функции действительно редки; в частности, поскольку C # не имеет большой (читай: вообще нет) поддержки хвостовой рекурсии.

  2. Существует гораздо более простой (более читабельный для непосвященных) способ определения псевдо-«анонимных» рекурсивных лямбд в C #:

    Func<int, int> fac = null;
    fac = x => x == 0 ? 1 : fac(x - 1) * x;
    

    Конечно, это не анонимно, но это "достаточно близко" для практических целей.

2 голосов
/ 22 января 2011

Хаскелл Карри изобрел Y-комбинатор для определения и использования анонимных рекурсивных функций в нетипизированном лямбда-исчислении, определяемом следующим образом:
Y = λf·(λx·f (x x)) (λx·f (x x))

Мое определение опровергает первоначальную цель Y-комбинатора, поскольку оно опирается на собственную поддержку C # для определения рекурсивных функций. Однако он все еще полезен тем, что позволяет определять анонимные рекурсивные функции в C #.

...