Что такое Y-комбинатор?
Y-комбинатор - это концепция информатики с "функциональной" стороны вещей. Большинство программистов вообще ничего не знают о комбинаторах, если они даже слышали о них.
- Что такое Y-комбинатор?
- Как работают комбинаторы?
- Для чего они хороши?
- Полезны ли они на процедурных языках?
18 ответов
Если вы готовы к длительному чтению, у Майка Ванье есть отличное объяснение. Короче говоря, он позволяет вам реализовать рекурсию на языке, который не обязательно поддерживает ее изначально.
Y-комбинатор - это "функционал" (функция, которая работает с другими функциями), которая включает рекурсию, когда вы не можете обратиться к функции изнутри. В теории информатики она обобщает рекурсию, абстрагирует ее реализацию и тем самым отделяет ее от фактической работы рассматриваемой функции. Преимущество отсутствия необходимости в имени компиляции для рекурсивной функции является своего рода бонусом. знак равно
Это применимо к языкам, которые поддерживают лямбда-функции. Природа лямбд на основе выражения обычно означает, что они не могут ссылаться на себя по имени. И обходить это путем объявления переменной, обращения к ней, а затем присвоения ей лямбды, чтобы завершить цикл самоссылки, является хрупким. Лямбда-переменная может быть скопирована, а исходная переменная переназначена, что нарушает самоссылку.
Y-комбинаторы громоздки для реализации и часто используются в статически-типизированных языках (которыми часто являются процедурные языки), потому что обычно ограничения типа требуют, чтобы число аргументов для рассматриваемой функции было известно во время компиляции. Это означает, что y-комбинатор должен быть записан для любого количества аргументов, которое нужно использовать.
Ниже приведен пример использования и работы Y-Combinator в C#.
Использование Y-комбинатора предполагает "необычный" способ построения рекурсивной функции. Сначала вы должны написать свою функцию в виде фрагмента кода, который вызывает уже существующую функцию, а не себя:
// Factorial, if func does the same thing as this bit of code...
x == 0 ? 1: x * func(x - 1);
Затем вы превращаете это в функцию, которая принимает функцию для вызова и возвращает функцию, которая делает это. Это называется функционалом, потому что он берет одну функцию и выполняет с ней операцию, которая приводит к другой функции.
// A function that creates a factorial, but only if you pass in
// a function that does what the inner function is doing.
Func<Func<Double, Double>, Func<Double, Double>> fact =
(recurs) =>
(x) =>
x == 0 ? 1 : x * recurs(x - 1);
Теперь у вас есть функция, которая принимает функцию и возвращает другую функцию, которая выглядит как факториал, но вместо вызова самой себя она вызывает аргумент, переданный во внешнюю функцию. Как вы делаете это факториалом? Передайте внутреннюю функцию себе. Y-Combinator делает это, будучи функцией с постоянным именем, которая может вводить рекурсию.
// One-argument Y-Combinator.
public static Func<T, TResult> Y<T, TResult>(Func<Func<T, TResult>, Func<T, TResult>> F)
{
return
t => // A function that...
F( // Calls the factorial creator, passing in...
Y(F) // The result of this same Y-combinator function call...
// (Here is where the recursion is introduced.)
)
(t); // And passes the argument into the work function.
}
Вместо того чтобы сам факториальный вызов вызывался, факториал вызывает факториальный генератор (возвращаемый рекурсивным вызовом Y-Combinator). И в зависимости от текущего значения t функция, возвращаемая генератором, либо снова вызовет генератор, с t - 1, либо просто вернет 1, завершив рекурсию.
Это сложно и загадочно, но все встряхивается во время выполнения, и ключом к его работе является "отложенное выполнение" и разбиение рекурсии на две функции. Внутренний F передается в качестве аргумента для вызова на следующей итерации, только если это необходимо.
Я взял это из http://www.mail-archive.com/boston-pm@mail.pm.org/msg02716.html которое является объяснением, которое я написал несколько лет назад.
Я буду использовать JavaScript в этом примере, но многие другие языки также будут работать.
Наша цель - написать рекурсивную функцию из 1 переменной, используя только функции из 1 переменной, без присваиваний, определить вещи по имени и т. Д. (Почему это наша цель - другой вопрос, давайте просто примем это как вызов, который мы.) Кажется невозможным, а? В качестве примера давайте реализуем факториал.
Ну, шаг 1 - сказать, что мы могли бы сделать это легко, если бы немного обманули. Используя функции 2 переменных и присваивания, мы можем по крайней мере избежать использования присваивания для настройки рекурсии.
// Here's the function that we want to recurse.
X = function (recurse, n) {
if (0 == n)
return 1;
else
return n * recurse(recurse, n - 1);
};
// This will get X to recurse.
Y = function (builder, n) {
return builder(builder, n);
};
// Here it is in action.
Y(
X,
5
);
Теперь посмотрим, сможем ли мы обмануть меньше. Ну, во-первых, мы используем назначение, но нам не нужно. Мы можем просто написать X и Y в строке.
// No assignment this time.
function (builder, n) {
return builder(builder, n);
}(
function (recurse, n) {
if (0 == n)
return 1;
else
return n * recurse(recurse, n - 1);
},
5
);
Но мы используем функции от 2 переменных, чтобы получить функцию от 1 переменной. Мы можем это исправить? У умного парня по имени Haskell Curry есть хитрый трюк, если у вас есть хорошие функции более высокого порядка, то вам нужны только функции с 1 переменной. Доказательством является то, что вы можете получить из функций 2 (или более в общем случае) переменных 1 переменную с помощью чисто механического преобразования текста, например:
// Original
F = function (i, j) {
...
};
F(i,j);
// Transformed
F = function (i) { return function (j) {
...
}};
F(i)(j);
где... остается точно таким же. (Этот трюк назван "карри" по имени его изобретателя. Язык Haskell также назван по имени Haskell Curry. Файл, который находится под бесполезными мелочами.) Теперь просто примените это преобразование везде, и мы получим нашу окончательную версию.
// The dreaded Y-combinator in action!
function (builder) { return function (n) {
return builder(builder)(n);
}}(
function (recurse) { return function (n) {
if (0 == n)
return 1;
else
return n * recurse(recurse)(n - 1);
}})(
5
);
Не стесняйтесь попробовать это. alert(), которые возвращают, привязывают к кнопке, что угодно. Этот код вычисляет факториалы рекурсивно, без использования присваивания, объявлений или функций двух переменных. (Но попытка проследить, как это работает, может привести к тому, что у вас закружится голова. И если передать его без вывода, просто немного переформатировать, то это приведет к тому, что код будет сбивать с толку и сбивать с толку.)
Вы можете заменить 4 строки, которые рекурсивно определяют факториал, любой другой рекурсивной функцией, которую вы хотите.
Интересно, есть ли смысл пытаться построить это с нуля. Посмотрим. Вот основная рекурсивная факториальная функция:
function factorial(n) {
return n == 0 ? 1 : n * factorial(n - 1);
}
Давайте рефакторинг и создадим новую функцию под названием fact
который возвращает анонимную функцию вычисления факториала вместо выполнения самого вычисления:
function fact() {
return function(n) {
return n == 0 ? 1 : n * fact()(n - 1);
};
}
var factorial = fact();
Это немного странно, но в этом нет ничего плохого. Мы просто генерируем новую факториальную функцию на каждом шаге.
На этом этапе рекурсия все еще довольно явная. fact
Функция должна знать свое имя. Давайте параметризуем рекурсивный вызов:
function fact(recurse) {
return function(n) {
return n == 0 ? 1 : n * recurse(n - 1);
};
}
function recurser(x) {
return fact(recurser)(x);
}
var factorial = fact(recurser);
Это здорово, но recurser
все еще нужно знать его собственное имя. Давайте также параметризуем это:
function recurser(f) {
return fact(function(x) {
return f(f)(x);
});
}
var factorial = recurser(recurser);
Теперь вместо звонка recurser(recurser)
Теперь давайте создадим функцию-оболочку, которая возвращает результат:
function Y() {
return (function(f) {
return f(f);
})(recurser);
}
var factorial = Y();
Теперь мы можем избавиться от recurser
имя в целом; это просто аргумент внутренней функции Y, который можно заменить самой функцией:
function Y() {
return (function(f) {
return f(f);
})(function(f) {
return fact(function(x) {
return f(f)(x);
});
});
}
var factorial = Y();
Единственное внешнее имя, на которое все еще ссылаются, fact
, но теперь должно быть ясно, что это легко параметризуется, создавая полное, общее решение:
function Y(le) {
return (function(f) {
return f(f);
})(function(f) {
return le(function(x) {
return f(f)(x);
});
});
}
var factorial = Y(function(recurse) {
return function(n) {
return n == 0 ? 1 : n * recurse(n - 1);
};
});
Большинство ответов выше описывают, что такое Y-комбинатор , но не то, для чего он.
Комбинаторы с фиксированной запятой используются, чтобы показать, что лямбда-исчисление завершено. Это очень важный результат в теории вычислений и обеспечивает теоретическую основу для функционального программирования.
Изучение комбинаторов с фиксированной запятой также помогло мне понять функциональное программирование. Я никогда не нашел их применения в реальном программировании.
Для программистов, которые не сталкивались с функциональным программированием в глубине и не хотят начинать сейчас, но немного любопытны:
Y-комбинатор - это формула, которая позволяет реализовать рекурсию в ситуации, когда функции не могут иметь имен, но могут передаваться как аргументы, использоваться как возвращаемые значения и определяться в других функциях.
Он работает, передавая функцию себе в качестве аргумента, чтобы он мог вызывать сам себя.
Это часть лямбда-исчисления, которое на самом деле математическое, но фактически является языком программирования и довольно фундаментально для компьютерных наук и особенно для функционального программирования.
Повседневная практическая ценность Y-комбинатора ограничена, поскольку языки программирования, как правило, позволяют называть функции.
В случае, если вам нужно идентифицировать его в полицейском составе, это выглядит так:
Y = λf. (Λx.f (x x)) (λx.f (x x))
Обычно вы можете заметить это из-за повторного (λx.f (x x))
,
λ
символами являются греческая буква лямбда, которая дает название лямбда-исчисления, и есть много (λx.t)
термины стиля, потому что так выглядит лямбда-исчисление.
Анонимная рекурсия
Комбинатор с фиксированной точкой является функцией высшего порядка fix
что по определению удовлетворяет эквивалентности
forall f. fix f = f (fix f)
fix f
представляет решение x
к уравнению с фиксированной точкой
x = f x
Факториал натурального числа может быть доказан
fact 0 = 1
fact n = n * fact (n - 1)
С помощью fix
произвольные конструктивные доказательства над общими / µ-рекурсивными функциями могут быть получены без одноименной самоссылки.
fact n = (fix fact') n
где
fact' rec n = if n == 0
then 1
else n * rec (n - 1)
такой, что
fact 3
= (fix fact') 3
= fact' (fix fact') 3
= if 3 == 0 then 1 else 3 * (fix fact') (3 - 1)
= 3 * (fix fact') 2
= 3 * fact' (fix fact') 2
= 3 * if 2 == 0 then 1 else 2 * (fix fact') (2 - 1)
= 3 * 2 * (fix fact') 1
= 3 * 2 * fact' (fix fact') 1
= 3 * 2 * if 1 == 0 then 1 else 1 * (fix fact') (1 - 1)
= 3 * 2 * 1 * (fix fact') 0
= 3 * 2 * 1 * fact' (fix fact') 0
= 3 * 2 * 1 * if 0 == 0 then 1 else 0 * (fix fact') (0 - 1)
= 3 * 2 * 1 * 1
= 6
Это формальное доказательство того, что
fact 3 = 6
методично использует эквивалентность комбинатора с фиксированной запятой для перезаписей
fix fact' -> fact' (fix fact')
Лямбда-исчисление
Нетипизированный формализм лямбда-исчисления состоит из контекстно-свободной грамматики
E ::= v Variable
| λ v. E Abstraction
| E E Application
где v
диапазоны по переменным, вместе с бета и правилами сокращения эта
(λ x. B) E -> B[x := E] Beta
λ x. E x -> E if x doesn’t occur free in E Eta
Бета-сокращение заменяет все свободные вхождения переменной x
в теле абстракции ("функции") B
выражением ("аргумент") E
, Эта сокращение уменьшает избыточную абстракцию. Иногда это исключается из формализма. Неприводимое выражение, к которому не применяется правило редукции, имеет нормальную или каноническую форму.
λ x y. E
это сокращение для
λ x. λ y. E
(абстракция мультиарность),
E F G
это сокращение для
(E F) G
(приложение левого ассоциативности),
λ x. x
а также
λ y. y
альфа-эквивалент.
Абстракция и приложение - это только два "языковых примитива" лямбда-исчисления, но они позволяют кодировать произвольно сложные данные и операции.
Церковные цифры представляют собой кодировку натуральных чисел, аналогичную пеаноаксиоматическим натуралам.
0 = λ f x. x No application
1 = λ f x. f x One application
2 = λ f x. f (f x) Twofold
3 = λ f x. f (f (f x)) Threefold
. . .
SUCC = λ n f x. f (n f x) Successor
ADD = λ n m f x. n f (m f x) Addition
MULT = λ n m f x. n (m f) x Multiplication
. . .
Формальное доказательство того, что
1 + 2 = 3
используя правило перезаписи бета-сокращения:
ADD 1 2
= (λ n m f x. n f (m f x)) (λ g y. g y) (λ h z. h (h z))
= (λ m f x. (λ g y. g y) f (m f x)) (λ h z. h (h z))
= (λ m f x. (λ y. f y) (m f x)) (λ h z. h (h z))
= (λ m f x. f (m f x)) (λ h z. h (h z))
= λ f x. f ((λ h z. h (h z)) f x)
= λ f x. f ((λ z. f (f z)) x)
= λ f x. f (f (f x)) Normal form
= 3
Комбинаторы
В лямбда-исчислении комбинаторы - это абстракции, которые не содержат свободных переменных. Проще всего: I
, личность комбинатор
λ x. x
изоморфна тождественной функции
id x = x
Такие комбинаторы являются примитивными операторами исчислений комбинаторов, таких как система SKI.
S = λ x y z. x z (y z)
K = λ x y. x
I = λ x. x
Бета-снижение не сильно нормализуется; не все приводимые выражения, "redexes", сходятся к нормальной форме при бета-редукции. Простой пример - расходящееся применение омеги ω
комбинатор
λ x. x x
к себе:
(λ x. x x) (λ y. y y)
= (λ y. y y) (λ y. y y)
. . .
= _|_ Bottom
Сокращение крайних левых подвыражений ("головы") является приоритетным. Аппликативный порядок нормализует аргументы перед заменой, нормальный порядок - нет. Две стратегии аналогичны активной оценке, например, C, и ленивой оценке, например, Haskell.
K (I a) (ω ω)
= (λ k l. k) ((λ i. i) a) ((λ x. x x) (λ y. y y))
расходится под стремительное снижение бета аппликативного порядка
= (λ k l. k) a ((λ x. x x) (λ y. y y))
= (λ l. a) ((λ x. x x) (λ y. y y))
= (λ l. a) ((λ y. y y) (λ y. y y))
. . .
= _|_
т.к. в строгой семантике
forall f. f _|_ = _|_
но сходится при ленивом уменьшении бета нормального порядка
= (λ l. ((λ i. i) a)) ((λ x. x x) (λ y. y y))
= (λ l. a) ((λ x. x x) (λ y. y y))
= a
Если выражение имеет нормальную форму, бета-редукция нормального порядка найдет его.
Y
Основное свойство Y
комбинатор с фиксированной запятой
λ f. (λ x. f (x x)) (λ x. f (x x))
дан кем-то
Y g
= (λ f. (λ x. f (x x)) (λ x. f (x x))) g
= (λ x. g (x x)) (λ x. g (x x)) = Y g
= g ((λ x. g (x x)) (λ x. g (x x))) = g (Y g)
= g (g ((λ x. g (x x)) (λ x. g (x x)))) = g (g (Y g))
. . . . . .
Эквивалентность
Y g = g (Y g)
изоморфен
fix f = f (fix f)
Нетипизированное лямбда-исчисление может кодировать произвольные конструктивные доказательства над общими / µ-рекурсивными функциями.
FACT = λ n. Y FACT' n
FACT' = λ rec n. if n == 0 then 1 else n * rec (n - 1)
FACT 3
= (λ n. Y FACT' n) 3
= Y FACT' 3
= FACT' (Y FACT') 3
= if 3 == 0 then 1 else 3 * (Y FACT') (3 - 1)
= 3 * (Y FACT') (3 - 1)
= 3 * FACT' (Y FACT') 2
= 3 * if 2 == 0 then 1 else 2 * (Y FACT') (2 - 1)
= 3 * 2 * (Y FACT') 1
= 3 * 2 * FACT' (Y FACT') 1
= 3 * 2 * if 1 == 0 then 1 else 1 * (Y FACT') (1 - 1)
= 3 * 2 * 1 * (Y FACT') 0
= 3 * 2 * 1 * FACT' (Y FACT') 0
= 3 * 2 * 1 * if 0 == 0 then 1 else 0 * (Y FACT') (0 - 1)
= 3 * 2 * 1 * 1
= 6
(Умножение отложено, слияние)
Было показано, что для нетипизированного лямбда-исчисления Черча существует рекурсивно перечислимая бесконечность комбинаторов с фиксированной запятой, кроме Y
,
X = λ f. (λ x. x x) (λ x. f (x x))
Y' = (λ x y. x y x) (λ y x. y (x y x))
Z = λ f. (λ x. f (λ v. x x v)) (λ x. f (λ v. x x v))
Θ = (λ x y. y (x x y)) (λ x y. y (x x y))
. . .
Бета-редукция нормального порядка делает нерасширенное нетипизированное лямбда-исчисление системой переписывания по Тьюрингу.
В Haskell элегантно реализован комбинатор с фиксированной точкой
fix :: forall t. (t -> t) -> t
fix f = f (fix f)
Лень Хаскелла нормализуется до конечности до того, как все подвыражения будут оценены.
primes :: Integral t => [t]
primes = sieve [2 ..]
where
sieve = fix (\ rec (p : ns) ->
p : rec [n | n <- ns
, n `rem` p /= 0])
- Дэвид Тернер: тезис Черча и функциональное программирование
- Церковь Алонзо: неразрешимая проблема элементарной теории чисел
- Лямбда-исчисление
- Теорема Черча – Россера
y-комбинатор в JavaScript:
var Y = function(f) {
return (function(g) {
return g(g);
})(function(h) {
return function() {
return f(h(h)).apply(null, arguments);
};
});
};
var factorial = Y(function(recurse) {
return function(x) {
return x == 0 ? 1 : x * recurse(x-1);
};
});
factorial(5) // -> 120
Редактировать: я многому научился, глядя на код, но этот немного сложно проглотить без некоторого фона - извините за это. С некоторыми общими знаниями, представленными другими ответами, вы можете начать разбирать на части то, что происходит.
Функция Y - это y-комбинатор. Теперь взгляните на var factorial
линия, где используется Y Обратите внимание, что вы передаете ему функцию с параметром (в этом примере recurse
), который также используется позже во внутренней функции. Имя параметра в основном становится именем внутренней функции, позволяющей ему выполнять рекурсивный вызов (так как он использует recurse()
в своем определении.) Y-комбинатор выполняет магическое связывание анонимной внутренней функции с именем параметра функции, передаваемой Y.
Для полного объяснения того, как Y делает волшебство, проверил связанную статью (не мной кстати).
Другие ответы дают довольно краткий ответ на этот вопрос, без одного важного факта: вам не нужно реализовывать комбинатор с фиксированной точкой на любом практическом языке таким сложным образом, и это не имеет никакого практического смысла (кроме: "Посмотрите, я знаю, что такое Y-комбинатор"). является"). Это важная теоретическая концепция, но мало практической ценности.
Вот JavaScript-реализация Y-Combinator и функции Factorial (из статьи Дугласа Крокфорда, доступной по адресу: http://javascript.crockford.com/little.html).
function Y(le) {
return (function (f) {
return f(f);
}(function (f) {
return le(function (x) {
return f(f)(x);
});
}));
}
var factorial = Y(function (fac) {
return function (n) {
return n <= 2 ? n : n * fac(n - 1);
};
});
var number120 = factorial(5);
Я написал своего рода "руководство для идиотов" для Y-Combinator как в Clojure, так и в Схеме, чтобы помочь себе в этом разобраться. На них влияет материал в "Маленьком интриганке"
В схеме: https://gist.github.com/z5h/238891
или Clojure: https://gist.github.com/z5h/5102747
Оба руководства содержат код с комментариями и должны быть вставлены в ваш любимый редактор.
Как новичок в комбинаторах, я нашел статью Майка Ваньера (спасибо Николасу Манкузо) очень полезной. Я хотел бы написать резюме, помимо документирования моего понимания, если бы оно могло помочь некоторым другим, я был бы очень рад.
От дрянного к менее дрянному
Используя факториал в качестве примера, мы используем следующее almost-factorial
функция для вычисления факториала числа x
:
def almost-factorial f x = if iszero x
then 1
else * x (f (- x 1))
В псевдокоде выше almost-factorial
принимает в функции f
и номер x
(almost-factorial
карри, так что это можно рассматривать как принятие в функции f
и возвращая 1-арную функцию).
когда almost-factorial
рассчитывает факториал для x
делегирует расчет факториала для x - 1
функционировать f
и накапливает этот результат с x
(в этом случае он умножает результат (x - 1) на x).
Это можно увидеть как almost-factorial
принимает дрянную версию факториальной функции (которая может вычислять только до числа x - 1
) и возвращает менее дрянную версию факториала (которая вычисляет до числа x
). Как в этой форме:
almost-factorial crappy-f = less-crappy-f
Если мы неоднократно передаем менее дрянную версию факториала almost-factorial
в конечном итоге мы получим желаемую факториальную функцию f
, Где это можно рассматривать как:
almost-factorial f = f
Fix-точка
Дело в том, что almost-factorial f = f
средства f
это точка фиксации функции almost-factorial
,
Это был действительно интересный способ увидеть взаимосвязь функций, описанных выше, и это был момент для меня. (пожалуйста, прочитайте сообщение Майка о точках, если вы этого не сделали)
Три функции
Обобщая, мы имеем нерекурсивную функцию fn
(как наш почти факторный), у нас есть его функция фиксированной точки fr
(как наш F), то, что Y
делает, когда вы даете Y
fn
, Y
возвращает функцию с фиксированной точкой fn
,
Итак, в итоге (упрощается, если предположить, fr
принимает только один параметр; x
вырождается в x - 1
, x - 2
... в рекурсии)
- Мы определяем основные расчеты как
fn
:def fn fr x = ...accumulate x with result from (fr (- x 1))
это почти полезная функция - хотя мы не можем использоватьfn
прямо наx
, будет полезно очень скоро. Это нерекурсивныйfn
использует функциюfr
рассчитать его результат fn fr = fr
,fr
является точкой отсчетаfn
,fr
полезная функция, мы можем использоватьfr
наx
чтобы получить наш результатY fn = fr
,Y
возвращает точку фиксации функции,Y
превращает нашу почти полезную функциюfn
в полезноеfr
Выведение Y
(не включено)
Я пропущу вывод Y
и идти к пониманию Y
, В сообщении Майка Вайнера много деталей.
Форма Y
Y
определяется как (в формате лямбда-исчисления):
Y f = λs.(f (s s)) λs.(f (s s))
Если мы заменим переменную s
слева от функций получаем
Y f = λs.(f (s s)) λs.(f (s s))
=> f (λs.(f (s s)) λs.(f (s s)))
=> f (Y f)
Так что, действительно, результат (Y f)
является точкой отсчета f
,
Почему (Y f)
Работа?
В зависимости от подписи f
, (Y f)
может быть функцией любой арности, чтобы упростить, давайте предположим, (Y f)
принимает только один параметр, как наша факторная функция.
def fn fr x = accumulate x (fr (- x 1))
поскольку fn fr = fr
, мы продолжаем
=> accumulate x (fn fr (- x 1))
=> accumulate x (accumulate (- x 1) (fr (- x 2)))
=> accumulate x (accumulate (- x 1) (accumulate (- x 2) ... (fn fr 1)))
рекурсивный расчет заканчивается, когда самый внутренний (fn fr 1)
это базовый случай и fn
не использует fr
в расчете.
Смотря на Y
снова:
fr = Y fn = λs.(fn (s s)) λs.(fn (s s))
=> fn (λs.(fn (s s)) λs.(fn (s s)))
Так
fr x = Y fn x = fn (λs.(fn (s s)) λs.(fn (s s))) x
Для меня волшебными частями этой установки являются:
fn
а такжеfr
взаимозависимы друг от друга:fr
"обручи"fn
внутри каждый разfr
используется для расчетаx
это "порождает" ("поднимает"?)fn
и делегирует расчет к этомуfn
(проходя в себеfr
а такжеx
); с другой стороны,fn
зависит отfr
и используетfr
рассчитать результат меньшей задачиx-1
,- В это время
fr
используется для определенияfn
(когдаfn
использованияfr
в своих операциях), реальнаяfr
еще не определено. - Это
fn
который определяет реальную бизнес-логику. На основеfn
,Y
создаетfr
- вспомогательная функция в определенной форме - для облегчения расчета дляfn
в рекурсивной манере.
Это помогло мне понять Y
Таким образом, на данный момент, надеюсь, это поможет.
Кстати, я также нашел книгу "Введение в функциональное программирование с помощью лямбда-исчисления" очень хорошей, я только часть этого и тот факт, что я не мог разобраться Y
в книге привел меня к этому посту.
Здесь приведены ответы на оригинальные вопросы, составленные из статьи (которую ВСЕГО стоит прочитать), упомянутой в ответе Николаса Манкузо, а также другие ответы:
Что такое Y-комбинатор?
Y-комбинатор - это "функционал" (или функция высшего порядка - функция, которая работает с другими функциями), которая принимает один аргумент, который является нерекурсивной функцией, и возвращает версию функции, которая является рекурсивный.
Несколько рекурсивно =), но более глубокое определение:
Комбинатор - это просто лямбда-выражение без свободных переменных.
Свободная переменная - это переменная, которая не является связанной переменной.
Связанная переменная - переменная, которая содержится в теле лямбда-выражения, в котором в качестве одного из аргументов указано имя этой переменной.
Еще один способ думать об этом заключается в том, что комбинатор - это такое лямбда-выражение, в котором вы можете заменить имя комбинатора на его определение везде, где оно найдено, и все будет работать (вы попадете в бесконечный цикл, если комбинатор содержать ссылку на себя, внутри лямбда-тела).
Y-комбинатор - это комбинатор с фиксированной точкой.
Фиксированная точка функции - это элемент области функции, который сопоставлен с ней самой функцией.
То есть c
является фиксированной точкой функции f(x)
если f(c) = c
Это означает f(f(...f(c)...)) = fn(c) = c
Как работают комбинаторы?
Примеры ниже предполагают сильную + динамическую типизацию:
Ленивый (нормальный порядок) Y-комбинатор:
Это определение применяется к языкам с отложенной (также отложенной, по требованию) оценкой - стратегией оценки, которая задерживает оценку выражения до тех пор, пока не потребуется его значение.
Y = λf.(λx.f(x x)) (λx.f(x x)) = λf.(λx.(x x)) (λx.f(x x))
Что это означает, что для данной функции f
(которая является нерекурсивной функцией), соответствующая рекурсивная функция может быть сначала получена путем вычисления λx.f(x x)
и затем применяя это лямбда-выражение к себе.
Строгий (аппликативный порядок) Y-комбинатор:
Это определение относится к языкам со строгой (также: жадной, жадной) оценкой - стратегией оценки, в которой выражение оценивается, как только оно связано с переменной.
Y = λf.(λx.f(λy.((x x) y))) (λx.f(λy.((x x) y))) = λf.(λx.(x x)) (λx.f(λy.((x x) y)))
Он такой же ленивый по своей природе, у него просто есть дополнительный λ
обертки, чтобы задержать оценку тела лямбды. Я задал еще один вопрос, несколько связанный с этой темой.
Для чего они хороши?
Украденное заимствовано из ответа Криса Аммермана: Y-комбинатор обобщает рекурсию, абстрагирует ее реализацию и тем самым отделяет ее от фактической работы рассматриваемой функции.
Несмотря на то, что Y-комбинатор имеет некоторые практические применения, это в основном теоретическая концепция, понимание которой расширит ваше общее видение и, вероятно, увеличит ваши аналитические и опытные навыки.
Полезны ли они на процедурных языках?
Как заявил Майк Ванье: во многих статически типизированных языках можно определить Y-комбинатор, но (по крайней мере в примерах, которые я видел) такие определения обычно требуют некоторого неочевидного взлома типов, поскольку сам Y-комбинатор не ' не имеет простой статический тип. Это выходит за рамки этой статьи, поэтому я не буду упоминать это дальше
И, как отметил Крис Аммерман: большинство процедурных языков имеют статическую типизацию.
Так что ответ на этот вопрос - не совсем.
Y-комбинатор реализует анонимную рекурсию. Так что вместо
function fib( n ){ if( n<=1 ) return n; else return fib(n-1)+fib(n-2) }
ты можешь сделать
function ( fib, n ){ if( n<=1 ) return n; else return fib(n-1)+fib(n-2) }
Конечно, Y-комбинатор работает только на языках по имени. Если вы хотите использовать это на любом обычном языке вызовов по значению, то вам понадобится соответствующий z-комбинатор (у-комбинатор будет расходиться / бесконечный цикл).
Комбинатор с фиксированной точкой (или оператор с фиксированной точкой) - это функция высшего порядка, которая вычисляет фиксированную точку других функций. Эта операция актуальна в теории языка программирования, потому что она позволяет реализовать рекурсию в форме правила перезаписи, без явной поддержки со стороны движка языка. (Википедия)
Оператор this может упростить вашу жизнь:
var Y = function(f) {
return (function(g) {
return g(g);
})(function(h) {
return function() {
return f.apply(h(h), arguments);
};
});
};
Тогда вы избегаете дополнительной функции:
var fac = Y(function(n) {
return n == 0 ? 1 : n * this(n - 1);
});
Наконец, вы звоните fac(5)
,
Я думаю, что лучший способ ответить на этот вопрос - выбрать язык, такой как JavaScript:
function factorial(num)
{
// If the number is less than 0, reject it.
if (num < 0) {
return -1;
}
// If the number is 0, its factorial is 1.
else if (num == 0) {
return 1;
}
// Otherwise, call this recursive procedure again.
else {
return (num * factorial(num - 1));
}
}
Теперь перепишите его так, чтобы оно не использовало имя функции внутри функции, но все равно вызывало его рекурсивно.
Единственное место название функции factorial
должно быть видно на сайте вызова.
Подсказка: вы не можете использовать имена функций, но вы можете использовать имена параметров.
Работа проблема. Не ищи это. Решив ее, вы поймете, какую проблему решает y-комбинатор.