it-swarm.com.ru

Что такое Y-комбинатор?

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

  • Что такое Y-комбинатор?
  • Как работают комбинаторы?
  • Для чего они хороши?
  • Полезны ли они на процедурных языках?
371
Chris Ammerman

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

194
Nicholas Mancuso

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 передается в качестве аргумента для вызова на следующей итерации, только при необходимости .

278
Chris Ammerman

Я поднял это из http://www.mail-archive.com/[email protected]/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 строки, которые рекурсивно определяют факториал, любой другой рекурсивной функцией, которую вы хотите.

98
btilly

Интересно, есть ли смысл пытаться построить это с нуля. Посмотрим. Вот основная рекурсивная факториальная функция:

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);
    };
});
80
Wayne Burkett

Большинство из приведенных выше ответов описывают, что такое Y-комбинатор , но не то, для чего он .

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

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

48
Jørgen Fogh

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 делает волшебство, проверил связанная статья (не мной кстати)

23
Zach

Для программистов, которые не сталкивались с функциональным программированием в глубине и не хотят начинать сейчас, но немного любопытны:

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

Он работает, передавая функцию себе в качестве аргумента, чтобы он мог вызывать сам себя.

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

Повседневная практическая ценность Y Combinator ограничена, так как языки программирования, как правило, позволяют вам называть функции.

В случае, если вам нужно идентифицировать его в полицейском составе, это выглядит так:

Y = λf. (Λx.f (x x)) (λx.f (x x))

Обычно вы можете обнаружить это из-за повторного (λx.f (x x)).

Символы λ - это греческая буква лямбда, которая дает название лямбда-исчислению, и существует множество терминов в стиле (λx.t), потому что именно так выглядит лямбда-исчисление.

17
El Zorko

Другие ответы дают довольно краткий ответ на этот вопрос, без одного важного факта: вам не нужно реализовывать комбинатор с фиксированной запятой на любом практическом языке таким замысловатым образом, и это не имеет никакого практического смысла (кроме "посмотрите, я знаю, что такое Y-комбинатор"). является"). Это важная теоретическая концепция, но мало практической ценности.

12
Ales Hakl

Вот 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);
6
xgMz

Y-Combinator - это другое название конденсатора.

6
Jon Davis

Я написал своего рода "руководство для идиотов" для Y-Combinator как в Clojure, так и в Схеме, чтобы помочь себе в этом разобраться. На них влияет материал в "Маленьком интриганке"

На схеме: https://Gist.github.com/z5h/238891

или Clojure: https://Gist.github.com/z5h/5102747

Оба руководства содержат код с комментариями и должны быть вставлены в ваш любимый редактор.

5
z5h

Анонимная рекурсия

Комбинатор с фиксированной запятой - это функция высшего порядка 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", сходятся к нормальной форме при бета-редукции. Простой пример - расходящееся применение комбинатора omega ω

λ x. x x

к себе:

   (λ x. x x) (λ y. y y)
=  (λ y. y y) (λ y. y y)
. . .
=  _|_                     Bottom

Сокращение крайних левых подвыражений ("головы") является приоритетным. Аппликативный порядок нормализует аргументы перед заменой, нормальный порядок - нет. Эти две стратегии аналогичны стремлению к оценке, например, С и ленивая оценка, например, 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])

5
user6428287

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-комбинатор (у-комбинатор будет расходиться/бесконечный цикл).

4
Andrew

Как новичок в комбинаторах, я нашел статья Майка Ванье (спасибо Николасу Манкузо) очень полезным. Я хотел бы написать резюме, помимо документирования моего понимания, если бы оно могло помочь некоторым другим, я был бы очень рад.

От Crappy до Less Crappy

Используя факториал в качестве примера, мы используем следующую функцию 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-arity).

Когда almost-factorial вычисляет факториал для x, он делегирует вычисление факториала для x - 1 функции f и накапливает этот результат с x (в этом случае он умножает результат (x - 1) на x).

Это можно увидеть как almost-factorial принимает crappy версию факториальной функции (которая может рассчитывать только до числа x - 1) и возвращает less-crappy версию факториала (которая рассчитывает до номер x). Как в этой форме:

almost-factorial crappy-f = less-crappy-f

Если мы несколько раз передадим версию факториала less-crappy в almost-factorial, мы в конечном итоге получим желаемую функцию факториала f. Где это можно рассматривать как:

almost-factorial f = f

Fix-точка

Тот факт, что almost-factorial f = f означает f, является фиксированной точкой функции almost-factorial.

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

Три функции

Обобщая, у нас есть нерекурсивная функция fn (как и наша почти факториальная), у нас есть ее исправление -point fr (как и наша f), тогда что Y делает, когда вы даете Yfn, 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 зависеть друг от друга друг на друга: frfn "Обертывания" внутри, каждый раз, когда fr используется для расчета x, это нерестится "("лифты"?) В fn и делегирует расчет в этой fn (проходящих в себе fr и x ); с другой стороны, fn зависит от fr и использует fr для вычисления результата меньшей задачи x-1.
  • В то время как fr используется для определения fn (когда fn использует fr в своих операциях), реальный fr еще не определен.
  • Это fn, который определяет настоящую бизнес-логику. На основе fnY создает fr - вспомогательную функцию в определенной форме - для облегчения вычисления для fn --- рекурсивно способом.

Это помогло мне понять Y на данный момент, надеюсь, это поможет.

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

4
Dapeng Li

Здесь приведены ответы на оригинальные вопросы , скомпилированные из статья (которая полностью стоит прочитать), упомянутые в ответ Николаса Манкузо , а также как и другие ответы:

Что такое Y-комбинатор?

Y-комбинатор - это "функционал" (или функция высшего порядка - функция, которая работает с другими функциями), который принимает один аргумент, который является нерекурсивной функцией, и возвращает версию функции, которая является рекурсивный.


Несколько рекурсивно =), но более подробное определение:

Комбинатор - это просто лямбда-выражение без свободных переменных.
Свободная переменная - это переменная, которая не является связанной переменной.
Связанная переменная - переменная, которая содержится в теле лямбда-выражения, в качестве аргумента которого используется имя этой переменной.

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

Y-комбинатор - это комбинатор с фиксированной точкой.

Фиксированная точка функции - это элемент области функции, который сопоставлен с ней самой функцией.
То есть c является фиксированной точкой функции f(x) if 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-combinator имеет некоторые практические применения, это в основном теоретическая концепция, понимание которой расширит ваше общее видение и, вероятно, увеличит ваши аналитические и опытные навыки.

Полезны ли они на процедурных языках?

Как заявлено Майком Ваньером : можно определить Y Combinator во многих статически типизированных языках, но (по крайней мере, в примерах, которые я видел ) такие определения обычно требуют некоторого неочевидного взлома типов, потому что сам Y Combinator не имеет прямого статического типа. Это выходит за рамки этой статьи, поэтому я не буду упоминать это дальше

И как упомянуто Крисом Аммерманом : большинство процедурных языков имеют статическую типизацию.

Так что ответ на этот - не совсем.

4
Filipp W.

Оператор 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).

3
Tires

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

3
Thomas Wagner

Я думаю, что лучший способ ответить на этот вопрос - выбрать язык, такой как 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-комбинатор.

0
zumalifeguard