Наверное все знают что в C# 3.0 лямбда выражения, которые позволяют записывать анонимные функции (то есть функции без имени).
Например:

seq.Select(x => x * x);

Выражение x => x*x является функцией одного аргумента и возвращает значение квадрата числа.

А теперь попробуем  записать функцию факториала:

Func<int, int> f = x => x > 1 ? x * f(x - 1) : 1;

Компилятор C# такое выражение не компилирует. Ругается на неинициализированную переменную f в правой части. Кстати roslyn такое прожевывает нормально. Тем не менее код выше не является выражением, его нельзя передать параметром в функцию.

Попробуем превратить его в выражение.

fact = f => x => x > 1 ? x * f(x - 1) : 1;

Тип выражения справа получится Func<Func<int,int>, Func<int,int>>, параметром передается рекурсивный вызов, чтобы его сформировать надо снова подставить рекурсивный вызов в функцию.  Получится что-то вроде бесконечного вызова
fact(fact(fact(fact(…. но реально число вызовов конечно.

Немного теории

Для функций f которая принимает аргумент и возвращают значения из одного и того же множества (на C# это записывается как Func<T,T>, а в математике T –> T) может существовать “неподвижная точка” x, для которой f(x) = x. Чтобы находить неподвижные точки можно построить комбинатор g:(T->T)->T, такой что g(f) = x и f(x) = x. Функция g называется комбинатором неподвижной точки.

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

Краткая формула для рекурсивного комбинатора неподвижной точки Y выглядит как Y(g) = g(Y(g)). Если выполнить подстановку то получится g(g(Y(g))), выполняя подстановку бесконечное число раз получим как раз то что нам нужно для факториала.

Вернемся к практике

Попробуем написать на C#

static T Y<T>(Func<T, T> f)
{
    return f(Y(f));
}

Но язык C# использует энергичные вычисления и Y-комбинатор сразу попытается посчитать бесконечную рекурсию. Что приведет к StackOverflowException.

Ленивость вычислений как всегда вводится через лямбды.

static Func<T, T> Y<T>(Func<Func<T, T>, Func<T, T>> f)
{
    return x => f(Y(f))(x);
}

После этого вполне можно написать следующий код:

var fact = Y<int>(f => x => x > 1 ? x * f(x - 1) : 1);

Или например

seq.Select(Y<int>(f => x => x > 1 ? x * f(x - 1) : 1));

Таким образом получили возможность записывать анонимную рекурсию в виде выражения.

Заключение

Знание фундаментальной теории очень помогает писать программы и зачастую дает возможность улучшить их крайне неожиданными способами. Изучение таких тем никогда не будет лишним багажом.

Теги : C#