Що таке Y-комбінатор? [зачинено]


392

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

  • Що таке Y-комбінатор?
  • Як працюють комбінатори?
  • Для чого вони хороші?
  • Чи корисні вони в процедурних мовах?

12
Трохи підказки, якщо ви
вивчаєте

3
Маю посміхнутися граватору редактора цього питання :) Пов’язане посилання в блозі
Мадса Торгенсена


1
Я написав короткий зміст, в якому поділився своїм розумінням Y-комбінатора: gist.github.com/houtianze/b274e4b975a28fe08aee681699c3f7d0 Я пояснив (наскільки я розумію) як "Y Combinator робить рекурсивну функцію"
ibic

1
Як це питання "занадто широке"?
Рей

Відповіді:


201

Якщо ви готові до довгого читання, Майк Ваньє має чудове пояснення . Коротше кажучи, це дозволяє реалізувати рекурсію мовою, яка не обов'язково підтримує її на самому місці.


14
Це трохи більше, ніж посилання; це посилання з дуже коротким підсумком . Більш детальне резюме буде вдячне.
Martijn Pieters

2
Це просто посилання, але НЕ може бути кращим за це. Ця відповідь заслуговує (додайте1 голос), не маючи жодної умови виходу; ака нескінченна рекурсія.
Явар

7
@Andre MacFie: Я не коментував зусилля, коментував якість. Загалом, політика щодо переповнення стека полягає в тому, що відповіді мають бути самостійними, із посиланнями на додаткову інформацію.
Jørgen Fogh

1
@galdre має рацію. Це чудове посилання, але це лише посилання. Про це також згадувалося в трьох інших відповідях нижче, але лише як підтверджуючий документ, оскільки вони все добре пояснюють самостійно. Ця відповідь навіть не намагається відповісти на запитання ОП.
торарітте

290

Y-комбінатор - це "функціонал" (функція, що функціонує на інших функціях), яка дозволяє рекурсію, коли ви не можете звертатися до функції зсередини. У теорії інформатики вона узагальнює рекурсію , абстрагуючи її реалізацію і тим самим відокремлюючи її від фактичної роботи відповідної функції. Користь від того, що не потрібна назва часу компіляції для рекурсивної функції, є свого роду бонусом. =)

Це стосується мов, які підтримують лямбда-функції . Вираз заснована природа лямбда зазвичай означає , що вони не можуть називати себе по імені. І обійти це шляхом оголошення змінної, переглянувшись на неї, потім присвоїти їй лямбда, щоб завершити цикл самопосилання, є крихким. Змінна лямбда може бути скопійована, а оригінальна змінна повторно присвоєна, що порушує самопосилання.

Y-комбінатори громіздкі для впровадження та часто використання в мовах статичного типу (які часто є процедурними мовами ), оскільки зазвичай обмеження введення вимагає, щоб кількість аргументів для відповідної функції була відома під час компіляції. Це означає, що y-комбінатор повинен бути записаний для будь-якого підрахунку аргументів, який потрібно використовувати.

Нижче наводиться приклад того, як використання та робота Y-комбінатора в 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 передається як аргумент , який слід викликати в наступній ітерації, лише за необхідності .


5
Чому о, чому вам довелося називати це "Y" і параметром "F"! Вони просто губляться в аргументах типу!
Брайан Хенк

3
У Haskell ви можете абстрагувати рекурсію за допомогою:, fix :: (a -> a) -> aа aможе, в свою чергу, бути функцією стільки аргументів, скільки вам потрібно. Це означає, що статичне введення насправді не робить це громіздким.
Пік

12
Відповідно до опису Майка Ваньє, ваше визначення для Y насправді не є комбінатором, оскільки воно є рекурсивним. У розділі "Усунення (найбільш) явної рекурсії (лінива версія)" він має ліниву схему, еквівалентну вашому коду C #, але пояснює в пункті 2: "Це не комбінатор, тому що Y в тілі визначення є вільною змінною, яка обмежується лише після того, як визначення буде завершене ... "Я думаю, що найзручніша річ у Y-комбінаторах полягає в тому, що вони виробляють рекурсію шляхом оцінки фіксованої точки функції. Таким чином вони не потребують явної рекурсії.
GrantJ

@GrantJ Ви добре зазначаєте. Минуло пару років, як я опублікував цю відповідь. Покірливий пост Ваньє зараз я бачу, що я написав Y, але не Y-Combinator. Незабаром я знову прочитаю його повідомлення і побачу, чи зможу я виправити виправлення. Моя кишка попереджає мене про те, що сувора статична типізація C # може запобігти її врешті-решт, але я побачу, що я можу зробити.
Кріс Аммерман

1
@WayneBurkett Це досить поширена практика з математики.
YoTengoUnLCD

102

Я підняв це з 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
);

Сміливо спробуйте. alar (), що повертаються, прив’яжіть його до кнопки, що завгодно. Цей код обчислює фактичні дані рекурсивно, не використовуючи призначення, декларації або функції 2 змінних. (Але спроба простежити, як це працює, швидше за все, змусить вашу голову крутитися. А передача її, без виведення, лише злегка переформатується, призведе до коду, який, безумовно, збиває з пантелику і заплутається.)

Ви можете замінити 4 рядки, які рекурсивно визначають факторіал, будь-якою іншою рекурсивною функцією, яку ви хочете.


Приємне пояснення. Чому ви писали function (n) { return builder(builder)(n);}замість builder(builder)?
v7d8dpo4

@ v7d8dpo4 Тому що я перетворював функцію 2 змінних у функцію вищого порядку однієї змінної за допомогою currying.
btilly

Це причина, що нам потрібні закриття?
TheChetan

1
@TheChetan Closures дозволить нам прив'язати індивідуальну поведінку за викликом до анонімної функції. Це просто ще одна техніка абстрагування.
btilly

85

Цікаво, чи є якась користь у спробах побудувати це з нуля. Подивимось. Ось основна, рекурсивна факторіальна функція:

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);
    };
});

Аналогічне пояснення у JavaScript: igstan.ro/posts/…
Pops

1
Ви втратили мене, коли запровадили функцію recurser. Не найменша ідея, що це робить, чи чому.
Мерре

2
Ми намагаємось створити загальне рекурсивне рішення для функцій, які не є явно рекурсивними. Ця recurserфункція є першим кроком до цієї мети, оскільки вона дає нам рекурсивну версію, factяка ніколи не посилається на ім'я.
Уейн

@WayneBurkett Можу чи я переписати Y комбінатор , як це: function Y(recurse) { return recurse(recurse); } let factorial = Y(creator => value => { return value == 0 ? 1 : value * creator(creator)(value - 1); });. І ось як я її перетравлюю (не впевнений, чи правильно): не чітко посилаючись на функцію (не дозволена як комбінатор ), ми можемо використовувати дві частково застосовані / викривлені функції (функція творця та функція обчислення), які ми можемо створити лямбда / анонімні функції, які досягають рекурсивності, не потребуючи імені для функції обчислення?
neevek

50

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

Нерухомі точки комбінаторів використовуються , щоб показати , що лямбда - обчислення є Тьюринг . Це дуже важливий результат в теорії обчислень і дає теоретичну основу для функціонального програмування .

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


24

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 робить магію, перегляньте зв'язану статтю (не мною btw.)


6
Javascript не потрібен Y-комбінатор зробити анонімну рекурсию , тому що ви можете отримати доступ до поточної функції з arguments.callee (див en.wikipedia.org/wiki / ... )
xitrium

6
arguments.calleeзаборонено в суворому режимі: developer.mozilla.org/uk/JavaScript/…
dave1010

2
Ви все одно можете давати ім’я будь-якій функції, і якщо це вираження функції, то це ім'я відомо лише всередині самої функції. (function fact(n){ return n <= 1? 1 : n * fact(n-1); })(5)
Есаїлія

1
за винятком IE. kangax.github.io/nfe
VoronoiPotato

18

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

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

Він працює, передаючи функцію собі як аргумент, тому може називати себе.

Це частина обчислення лямбда, яка справді є математикою, але фактично є мовою програмування, і є досить важливою для інформатики, особливо для функціонального програмування.

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

Якщо вам потрібно визначити це в поліцейському складі, це виглядає приблизно так:

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

Зазвичай ви можете помітити це через повторне (λx.f (x x)).

Ці λсимволи є грецька буква лямбда, який дає лямбда - обчислення його імені, і є багато (λx.t)термінів типу , тому що це те , що лямбда - обчислення виглядає.


це має бути прийнятою відповіддю. BTW, з U x = x x, Y = U . (. U)(зловживаючи нотатками, подібними до Haskell). IOW, з відповідними комбінаторів Y = BU(CBU). Таким чином, Yf = U (f . U) = (f . U) (f . U) = f (U (f . U)) = f ((f . U) (f . U)).
Буде Несс

13

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

Комбінатор з фіксованою точкою - це функція вищого порядку, 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])


4
Хоча я ціную ґрунтовність відповіді, вона жодним чином не доступна програмісту з невеликим офіційним математичним фоном після перерви першого рядка.
Джаред Сміт

4
@ jared-smith Відповідь має на меті розповісти додаткову історію Вонкайя про поняття CS / math, що стоять за комбінатором Y. Я думаю, що, мабуть, найкращі аналогії з знайомими поняттями були проведені вже іншими відповідачами. Особисто мені завжди подобалося стикатися з справжнім походженням, радикальною новизною ідеї, приємною аналогією. Я вважаю більшість подібних аналогій недоречними та заплутаними.

1
Привіт, комбінатор ідентичності λ x . x, як ти сьогодні?
MaiaVictor

Мені подобається ця відповідь найбільше . Це просто очистило всі мої запитання!
Студент

11

Інші відповіді дають досить стислий відповідь на це, не маючи жодного важливого факту: Вам не потрібно реалізовувати комбінатор з фіксованою точкою на будь-якій практичній мові цим звивистим способом, і це не виконує жодних практичних цілей (крім "дивіться, я знаю, що Y-комбінатор" є "). Це важлива теоретична концепція, але мала практичне значення.


6

Ось реалізація 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

Y-Combinator - інша назва флюсового конденсатора.


4
дуже смішно. :) молоді (ер), можливо, не визнають посилання.
Буде Несс

2
ха-ха! Так, молодий (я) ще може зрозуміти ...

Я подумав, що це реально, і я опинився тут. youtube.com/watch?v=HyWqxkaQpPw Остання стаття futurism.com/scientists-made-real-life-flux-capacitor
побачив мислитель Nay Htoo

Я думаю, що ця відповідь може бути особливо заплутаною для носіїв англійської мови. Можна приділити досить багато часу, щоб зрозуміти це твердження, перш ніж (чи ніколи) не зрозуміючи, що це гумористична посилання на популярну культуру. (Мені це подобається, я б просто почував себе погано, якби я відповів на це і виявив, що хтось, хто навчається, це не відштовхує)
Майк

5

Я написав свого роду "посібник для ідіотів" Y-комбінатору як в Clojure, так і в схемі, щоб допомогти собі впоратися з цим. На них впливає матеріал у "Маленькому шемері"

У схемі: https://gist.github.com/z5h/238891

або Clojure: https://gist.github.com/z5h/5102747

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


5

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

Від Crappy до менш 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займає шалену версію факторної функції (яка може обчислювати лише число x - 1) і повертає менш шалену версію факториалу (яка обчислює число x). Як у цій формі:

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

Якщо ми неодноразово передаємо менш хитру версію заводуalmost-factorial , ми зрештою отримаємо бажану функціональну функцію f. Де це може розглядатися як:

almost-factorial f = f

Фіксація

Справа в тому , що 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є корисним Funciton, ми можемо використовувати 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в книзі, привів мене до цієї публікації.


5

Ось відповіді на оригінальні запитання , зібрані зі статті (яку ВСІЧНО варто прочитати), згаданої у відповіді Ніколаса Манкусо , а також інших відповідей:

Що таке 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 не робить ' не мають прямого статичного типу. Це виходить за межі цієї статті, тому я не буду більше згадувати про це

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

Тож відповідь на це - не дуже.


4

У-комбінатор реалізує анонімну рекурсію. Тож замість

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


Y комбінатор може працювати з оцінкою прохідності та ледачості.
Quelklef

3

Комбінатор з фіксованою точкою (або оператор з фіксованою точкою) - це функція вищого порядку, яка обчислює фіксовану точку інших функцій. Ця операція є актуальною в теорії мови програмування, оскільки дозволяє реалізувати рекурсію у вигляді правила перезапису, без явної підтримки з мовою виконання програми. (src Wikipedia)


3

Цей оператор може спростити ваше життя:

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).


0

Я думаю, що найкращий спосіб відповісти на це - вибрати мову, наприклад 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-комбінатор.


1
Ви впевнені, що це не створює більше проблем, ніж вирішує?
Noctis Skytower

1
Noctis, ви можете уточнити своє запитання? Ви запитуєте, чи сама концепція y-комбінатора створює більше проблем, ніж вона вирішує, або ви конкретно говорите про те, що я вирішив продемонструвати, використовуючи JavaScript, або мою конкретну реалізацію чи мою рекомендацію вивчити її, відкривши її як Я описав?
zumalifeguard
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.