Y-combinator คืออะไร [ปิด]


392

Y-combinator เป็นแนวคิดวิทยาการคอมพิวเตอร์จาก "หน้าที่" ของสิ่งต่าง ๆ โปรแกรมเมอร์ส่วนใหญ่ไม่ค่อยรู้เรื่อง combinators เลยแม้แต่น้อยหากพวกเขาเคยได้ยินเกี่ยวกับพวกเขา

  • Y-combinator คืออะไร
  • ผู้ประสานงานทำงานอย่างไร
  • สิ่งที่ดีสำหรับ
  • มันมีประโยชน์ในภาษาเชิงโพรซีเดอร์หรือไม่?

12
บิตของเคล็ดลับถ้าคุณกำลังเรียนรู้เกี่ยวกับภาษาที่ใช้งานได้ดีกว่าปล่อยให้ combinators จนกว่าคุณจะคุ้นเคยกับมันมิฉะนั้นมันจะเป็น
หนทาง

3
ต้องยิ้มให้กับ gravatar ของผู้แก้ไขคำถามนี้ :) ลิงก์ที่เกี่ยวข้องในบล็อกของ Mads Torgensen
Benjol


1
ฉันเขียนส่วนสำคัญสั้น ๆ ที่แบ่งปันความเข้าใจของฉันเกี่ยวกับ Y Combinator: gist.github.com/houtianze/b274e4b975a28fe08aee681699c3f7d0ฉันอธิบาย (ความเข้าใจของฉัน) ว่า "Y Combinator ทำหน้าที่เรียกซ้ำได้อย่างไร"
ibic

1
คำถามนี้กว้างเกินไปหรือไม่
Rei Miyasaka

คำตอบ:


201

หากคุณพร้อมสำหรับการอ่านยาวไมค์เวเนียร์มีดีคำอธิบาย เรื่องสั้นสั้น ๆ ช่วยให้คุณสามารถใช้การเรียกซ้ำในภาษาที่ไม่จำเป็นต้องสนับสนุน


14
มันเป็นมากกว่าลิงค์ มันคือการเชื่อมโยงกับที่สรุปสั้นมาก สรุปอีกต่อไปจะได้รับการชื่นชม
Martijn Pieters

2
มันเป็นแค่ลิงค์ แต่มันก็ไม่ได้ดีไปกว่านี้อีกแล้ว คำตอบนี้สมควรได้รับ (เพิ่ม 1 โหวต) โดยไม่มีเงื่อนไขกรณีฐานเพื่อออก การเรียกซ้ำอนันต์ aka
Yavar

7
@ Andre MacFie: ฉันไม่ได้แสดงความคิดเห็นเกี่ยวกับความพยายามฉันให้ความเห็นเกี่ยวกับคุณภาพ โดยทั่วไปแล้วนโยบายเกี่ยวกับ Stack Overflow คือคำตอบควรมีอยู่ในตัวพร้อมลิงก์ไปยังข้อมูลเพิ่มเติม
Jørgen Fogh

1
@galdre ถูกต้อง มันเป็นลิงค์ที่ยอดเยี่ยม แต่เป็นเพียงลิงค์ นอกจากนี้ยังได้รับการกล่าวถึงใน 3 คำตอบอื่น ๆ ด้านล่าง แต่เป็นเอกสารประกอบเนื่องจากพวกเขาทุกคนมีคำอธิบายที่ดีด้วยตนเอง คำตอบนี้ยังไม่ได้พยายามตอบคำถามของ OP
toraritte

290

Y-combinator คือ "functional" (ฟังก์ชั่นที่ทำงานกับฟังก์ชั่นอื่น ๆ ) ที่เปิดใช้งานการเรียกซ้ำเมื่อคุณไม่สามารถอ้างถึงฟังก์ชั่นจากภายในตัวเอง ในทฤษฎีคอมพิวเตอร์ - วิทยาศาสตร์มันสรุปการเรียกซ้ำการสรุปการนำไปใช้และแยกมันออกจากการทำงานจริงของฟังก์ชันในคำถาม ประโยชน์ของการไม่ต้องการชื่อเวลาคอมไพล์สำหรับฟังก์ชันการเรียกซ้ำคือโบนัส =)

นี้มีผลบังคับใช้ในภาษาที่สนับสนุนฟังก์ชั่นแลมบ์ดา การแสดงออกตามธรรมชาติของลูกแกะมักจะหมายความว่าพวกเขาไม่สามารถอ้างถึงตัวเองด้วยชื่อ และหลีกเลี่ยงปัญหานี้โดยการประกาศตัวแปรอ้างถึงจากนั้นกำหนดแลมบ์ดาให้กับวงนั้นเพื่อให้การอ้างอิงด้วยตัวเองนั้นสมบูรณ์ ตัวแปรแลมบ์ดาสามารถคัดลอกและตัวแปรดั้งเดิมกำหนดใหม่ซึ่งแบ่งการอ้างอิงด้วยตนเอง

Y-combinators ยุ่งยากในการใช้และบ่อยครั้งที่จะใช้ในภาษาแบบคงที่ (ซึ่งภาษาขั้นตอนมักจะเป็น) เพราะปกติข้อ จำกัด การพิมพ์ต้องมีจำนวนข้อโต้แย้งสำหรับฟังก์ชั่นที่เป็นที่รู้จักในเวลารวบรวม ซึ่งหมายความว่า y-combinator ต้องถูกเขียนสำหรับอาร์กิวเมนต์ใด ๆ ที่เราต้องการใช้

ด้านล่างนี้เป็นตัวอย่างการใช้งานและการทำงานของ Y-Combinator ใน C #

การใช้ Y-combinator นั้นเกี่ยวข้องกับวิธี "ผิดปกติ" ในการสร้างฟังก์ชันแบบเรียกซ้ำ ก่อนอื่นคุณต้องเขียนฟังก์ชันของคุณเป็นโค้ดที่เรียกใช้ฟังก์ชันที่มีอยู่ก่อนแทนที่จะเป็นฟังก์ชั่น:

// 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'! พวกเขาเพิ่งหลงทางในประเภทอาร์กิวเมนต์!
Brian Henk

3
ใน Haskell คุณสามารถใช้การเรียกซ้ำแบบนามธรรมโดย: fix :: (a -> a) -> aและaสามารถเปลี่ยนเป็นฟังก์ชันของอาร์กิวเมนต์ได้มากเท่าที่คุณต้องการ ซึ่งหมายความว่าการพิมพ์แบบคงที่ไม่ได้ทำให้ยุ่งยากเท่านี้
Peaker

12
ตามคำอธิบายของ Mike Vanier คำจำกัดความของคุณสำหรับ Y ไม่ใช่ combinator จริง ๆเพราะเป็นการเรียกซ้ำ ภายใต้ "การกำจัด (เรียกซ้ำ (ชัดเจนที่สุด) เรียกซ้ำ (รุ่นที่ขี้เกียจ)" เขามีรูปแบบที่ขี้เกียจเทียบเท่ากับรหัส C # ของคุณ แต่อธิบายในจุดที่ 2: "มันไม่ใช่ combinator เพราะ Y ในเนื้อหาของคำจำกัดความเป็นตัวแปรอิสระ ถูกผูกมัดเมื่อคำจำกัดความเสร็จสมบูรณ์ ... "ฉันคิดว่าสิ่งที่ยอดเยี่ยมเกี่ยวกับ Y-combinators คือพวกเขาสร้างการสอบถามซ้ำโดยการประเมินจุดคงที่ของฟังก์ชั่น ด้วยวิธีนี้พวกเขาไม่ต้องการการสอบถามซ้ำอย่างชัดเจน
GrantJ

@ GrantJ คุณสร้างจุดดี เป็นเวลาสองปีแล้วที่ฉันโพสต์คำตอบนี้ โพสต์ของ Skimming Vanier ตอนนี้ฉันเห็นว่าฉันเขียน Y แต่ไม่ใช่ Y-Combinator ฉันจะอ่านโพสต์ของเขาอีกครั้งในไม่ช้าและดูว่าฉันสามารถโพสต์การแก้ไขได้หรือไม่ ลำไส้ของฉันเตือนฉันว่าการพิมพ์คงที่อย่างเข้มงวดของ C # อาจป้องกันได้ในท้ายที่สุด แต่ฉันจะเห็นสิ่งที่ฉันสามารถทำได้
Chris Ammerman

1
@WayneBurkett มันเป็นเรื่องธรรมดาในวิชาคณิตศาสตร์
YoTengoUnLCD

102

ฉันได้ยกสิ่งนี้จากhttp://www.mail-archive.com/boston-pm@mail.pm.org/msg02716.htmlซึ่งเป็นคำอธิบายที่ฉันเขียนเมื่อหลายปีก่อน

ฉันจะใช้จาวาสคริปต์ในตัวอย่างนี้ แต่ภาษาอื่น ๆ ก็ใช้งานได้เช่นกัน

เป้าหมายของเราคือเพื่อให้สามารถเขียนฟังก์ชันแบบเรียกซ้ำของ 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 ตัวแปร เราสามารถแก้ไขได้ไหม ถ้าคุณมีฟังก์ชั่นการสั่งซื้อที่ดีกว่าคุณจะต้องใช้ฟังก์ชั่น 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 () กลับมาผูกมันไว้กับปุ่มอะไรก็ได้ รหัสนั้นจะคำนวณแฟคทอเรียลแบบเรียกซ้ำโดยไม่ใช้การกำหนดการประกาศหรือฟังก์ชันของตัวแปร 2 ตัว (แต่พยายามที่จะติดตามว่ามันทำงานอย่างไรมีแนวโน้มที่จะทำให้หัวของคุณหมุนและส่งโดยไม่ได้รับมาเพียงฟอร์แมตใหม่เพียงเล็กน้อยจะส่งผลในรหัสที่แน่ใจว่าทำให้ยุ่งเหยิงและสับสน)

คุณสามารถแทนที่ 4 บรรทัดที่กำหนดแฟกทอเรียลแบบเรียกซ้ำด้วยฟังก์ชันเรียกซ้ำอื่น ๆ ที่คุณต้องการ


คำอธิบายที่ดี ทำไมคุณถึงเขียนfunction (n) { return builder(builder)(n);}แทนที่จะbuilder(builder)?
v7d8dpo4

@ v7d8dpo4 เพราะฉันเปลี่ยนฟังก์ชั่นของตัวแปร 2 ตัวให้กลายเป็นฟังก์ชั่นการเรียงลำดับที่สูงขึ้นของตัวแปรตัวหนึ่งโดยใช้การแกง
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)โดยตรงมาสร้างฟังก์ชั่น wrapper ที่ส่งคืนผลลัพธ์:

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คุณหายไปฉันเมื่อคุณแนะนำฟังก์ชั่น ไม่ใช่ความคิดเพียงเล็กน้อยว่ามันกำลังทำอะไรหรือเพราะอะไร
Mörre

2
เรากำลังพยายามสร้างโซลูชันแบบเรียกซ้ำโดยทั่วไปสำหรับฟังก์ชันที่ไม่ซ้ำแบบเรียกซ้ำ recurserฟังก์ชั่นเป็นขั้นตอนแรกไปสู่เป้าหมายนี้เพราะมันจะช่วยให้เราเป็นรุ่น recursive ของfactที่ไม่เคยอ้างอิงถึงตัวเองโดยใช้ชื่อ
Wayne

@WayneBurkett ฉันจะเขียน Y combinator อีกครั้งแบบนี้function Y(recurse) { return recurse(recurse); } let factorial = Y(creator => value => { return value == 0 ? 1 : value * creator(creator)(value - 1); });ได้ไหม และนี่คือวิธีที่ฉันแยกแยะมัน (ไม่แน่ใจว่ามันถูกต้องหรือไม่): โดยไม่อ้างอิงฟังก์ชันอย่างชัดเจน (ไม่ได้รับอนุญาตให้ใช้เป็นcombinator ) เราสามารถใช้ฟังก์ชันที่ประยุกต์ใช้ / curried ได้สองส่วน (ฟังก์ชันผู้สร้างและฟังก์ชันคำนวณ) ด้วย ซึ่งเราสามารถสร้างฟังก์ชั่นแลมบ์ดา / ไม่ระบุชื่อที่บรรลุแบบเรียกซ้ำโดยไม่ต้องใช้ชื่อสำหรับฟังก์ชั่นการคำนวณ
neevek

50

คำตอบส่วนใหญ่ข้างต้นอธิบายว่า Y-combinator นั้นคืออะไร แต่ไม่ใช่สำหรับอะไร

combinators จุดคงที่จะใช้ในการแสดงให้เห็นว่าแคลคูลัสแลมบ์ดาเป็นทัวริงสมบูรณ์ นี้เป็นผลที่สำคัญมากในทฤษฎีการคำนวณและให้รากฐานทางทฤษฎีสำหรับการเขียนโปรแกรมการทำงาน

การศึกษาผู้กำหนดจุดรวมคงช่วยให้ฉันเข้าใจการเขียนโปรแกรมที่ใช้งานได้จริง ฉันไม่เคยพบการใช้งานใด ๆ สำหรับพวกเขาในการเขียนโปรแกรมจริงแม้ว่า


24

y-combinator ใน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-combinator" ทีนี้ลองดูvar factorialบรรทัดที่ใช้ Y โปรดสังเกตว่าคุณส่งผ่านฟังก์ชันที่มีพารามิเตอร์ (ในตัวอย่างนี้recurse) ที่ใช้ในภายหลังในฟังก์ชันภายใน ชื่อพารามิเตอร์โดยทั่วไปจะกลายเป็นชื่อของฟังก์ชั่นภายในที่ช่วยให้มันสามารถทำการโทรซ้ำ (เพราะมันใช้recurse()ในการกำหนดมัน) y-combinator ดำเนินการมายากลของการเชื่อมโยงฟังก์ชั่นภายในที่ไม่ระบุชื่อมิฉะนั้นกับชื่อพารามิเตอร์ของฟังก์ชั่น วาย

สำหรับคำอธิบายที่สมบูรณ์เกี่ยวกับวิธีที่ Y ใช้เวทมนตร์ให้ตรวจสอบบทความที่เชื่อมโยง (ไม่ใช่โดยฉัน btw)


6
Javascript ไม่ต้องการ Y-combinator ในการเรียกซ้ำแบบไม่ระบุชื่อเนื่องจากคุณสามารถเข้าถึงฟังก์ชันปัจจุบันด้วย arguments.callee (ดูen.wikipedia.org/wiki/… )
xitrium

6
arguments.calleeไม่ได้รับอนุญาตในโหมดเข้มงวด: developer.mozilla.org/en/JavaScript/ …
dave1010

2
คุณยังสามารถให้ชื่อฟังก์ชั่นใด ๆ และถ้ามันเป็นฟังก์ชั่นการแสดงออกแล้วชื่อนั้นเป็นที่รู้จักกันเฉพาะในฟังก์ชั่นของตัวเอง (function fact(n){ return n <= 1? 1 : n * fact(n-1); })(5)
Esailija

1
ยกเว้นใน IE kangax.github.io/nfe
VoronoiPotato

18

สำหรับโปรแกรมเมอร์ที่ไม่ได้พบกับการเขียนโปรแกรมเชิงลึกและไม่สนใจที่จะเริ่มต้นตอนนี้ แต่อยากรู้อยากเห็น:

Y combinator เป็นสูตรที่ให้คุณใช้การเรียกซ้ำในสถานการณ์ที่ฟังก์ชันไม่สามารถมีชื่อ แต่สามารถส่งผ่านเป็นอาร์กิวเมนต์ใช้เป็นค่าส่งคืนและกำหนดภายในฟังก์ชันอื่น

มันทำงานได้โดยส่งผ่านฟังก์ชั่นไปยังตัวมันเองเพื่อเป็นข้อโต้แย้ง

มันเป็นส่วนหนึ่งของแคลคูลัสแลมบ์ดาซึ่งเป็นคณิตศาสตร์จริง ๆ แต่เป็นภาษาการเขียนโปรแกรมได้อย่างมีประสิทธิภาพและเป็นพื้นฐานของวิทยาศาสตร์คอมพิวเตอร์และโดยเฉพาะอย่างยิ่งกับการเขียนโปรแกรมที่ใช้งานได้

มูลค่าการใช้งานในแต่ละวันของเครื่อง Combinator 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)).
Will Ness

13

การเรียกซ้ำแบบไม่ระบุชื่อ

Combinator จุดคงที่เป็นฟังก์ชั่นการเรียงลำดับที่สูงกว่าfixซึ่งตามคำจำกัดความสอดคล้องกับความเท่าเทียม

forall f.  fix f  =  f (fix f)

fix fแสดงถึงวิธีการแก้xสมการคงที่

               x  =  f x

แฟกทอเรียลของจำนวนธรรมชาติสามารถพิสูจน์ได้ด้วย

fact 0 = 1
fact n = n * fact (n - 1)

การใช้fixการพิสูจน์เชิงสร้างสรรค์โดยพลการเหนือฟังก์ชั่นทั่วไป / μ-recursive สามารถรับได้โดยไม่มีการอ้างอิงตนเองแบบไม่สมมาตร

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

ใช้วิธีการเทียบเท่า combinator จุดคงที่สำหรับการเขียนใหม่

fix fact'  ->  fact' (fix fact')

แลมบ์ดาแคลคูลัส

untyped แลมบ์ดาแคลคูลัสพิธีประกอบด้วยในไวยากรณ์บริบทฟรี

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

ลด Beta ทดแทนเกิดขึ้นฟรีทั้งหมดของตัวแปรxในนามธรรม (“ฟังก์ชั่น”) ร่างกายBโดยการแสดงออก E(“ข้อโต้แย้ง”) การลด Eta ช่วยลดสิ่งที่ซ้ำซ้อน บางครั้งก็ถูกละไว้จากพิธีการ ลดลงไม่ได้แสดงออกซึ่งกฎการลดไม่มีผลบังคับใช้อยู่ในปกติหรือรูปแบบที่เป็นที่ยอมรับ

λ x y. E

เป็นการจดชวเลข

λ x. λ y. E

(นามธรรมหลายสิ่งที่เป็นนามธรรม)

E F G

เป็นการจดชวเลข

(E F) G

(การเชื่อมโยงด้านซ้ายของแอปพลิเคชัน)

λ x. x

และ

λ y. y

มีอัลฟาเทียบเท่า

สิ่งที่เป็นนามธรรมและการประยุกต์ใช้เป็นเพียง“ ดั้งเดิมภาษา” ของแคลคูลัสแลมบ์ดา แต่พวกเขาอนุญาตให้เข้ารหัสข้อมูลที่ซับซ้อนและการดำเนินงานโดยพลการ

เลขคริสตจักรเป็นการเข้ารหัสของตัวเลขธรรมชาติคล้ายกับธรรมชาติ Peano-axiomatic

   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

combinators

ในแคลคูลัสแลมบ์ดาผู้รวมภาพเป็นนามธรรมที่ไม่มีตัวแปรอิสระ เรียบง่ายที่สุด: Icombinator ตัวตน

λ x. x

isomorphic กับฟังก์ชันตัวตน

id x = x

Combinator ดังกล่าวเป็นตัวดำเนินการดั้งเดิมของCombinator Calculiเช่นระบบ SKI

S  =  λ x y z. x z (y z)
K  =  λ x y. x
I  =  λ x. x

ลดเบต้าไม่normalizing อย่างยิ่ง ; ไม่ใช่นิพจน์ที่ลดได้ทั้งหมด“ redexes”, รวมตัวกันเป็นรูปแบบปกติภายใต้การลดเบต้า ตัวอย่างง่ายๆคือแอปพลิเคชันที่แตกต่างกันของโอเมก้าωcombinator

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

diverges ภายใต้การลดลงของเบต้าแบบกระตือรือร้นสำหรับการสมัคร

=  (λ 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 combinator จุดคงที่

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

isomorphic ถึง

fix f  =  f (fix f)

แคลคูลัสแลมบ์ดาที่ไม่ได้พิมพ์สามารถเข้ารหัสข้อพิสูจน์เชิงสร้างสรรค์ตามอำเภอใจเหนือฟังก์ชันทั่วไป / μ-recursive

 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

(การคูณล่าช้ารวมกัน)

สำหรับแคลคูลัสแลมบ์ดาที่ไม่ได้พิมพ์ออกมาจากโบสถ์ก็มีการแสดงให้เห็นว่ามีจำนวนอนันต์นับไม่ถ้วนที่เรียกว่า combinators คงที่ที่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 สามารถติดตั้ง Combinator แบบจุดคงที่ได้อย่างหรูหรา

fix :: forall t. (t -> t) -> t
fix f = f (fix f)

ความเกียจคร้านของ Haskell จะทำให้เป็นเรื่องปกติก่อนที่จะมีการประเมิน subexpressions ทั้งหมด

primes :: Integral t => [t]
primes = sieve [2 ..]
   where
      sieve = fix (\ rec (p : ns) ->
                     p : rec [n | n <- ns
                                , n `rem` p /= 0])


4
ในขณะที่ฉันซาบซึ้งกับความละเอียดของคำตอบ แต่ก็ไม่สามารถเข้าถึงโปรแกรมเมอร์ที่มีพื้นหลังทางคณิตศาสตร์อย่างเป็นทางการได้เล็กน้อยหลังจากการแบ่งบรรทัดแรก
Jared Smith

4
@ jared-smith คำตอบมีไว้เพื่อบอกเล่าเรื่องราวของวองเคียนเพิ่มเติมเกี่ยวกับแนวคิด CS / คณิตศาสตร์ที่อยู่เบื้องหลัง combinator Y ฉันคิดว่าอาจเป็นไปได้ว่าการเปรียบเทียบที่ดีที่สุดกับแนวคิดที่คุ้นเคยได้ถูกดึงไปแล้วโดยผู้ตอบคำถามคนอื่น ๆ โดยส่วนตัวฉันชอบที่จะเผชิญหน้ากับต้นกำเนิดที่แท้จริงความแปลกใหม่ที่แปลกใหม่ของความคิดมากกว่าการเปรียบเทียบที่ดี ฉันพบว่าการเปรียบเทียบในวงกว้างไม่เหมาะสมและสับสน

1
สวัสดีผู้ประสานข้อมูลประจำλ x . xวันคุณเป็นอย่างไรบ้าง
MaiaVictor

ผมชอบคำตอบนี้มากที่สุด มันแค่ล้างคำถามทั้งหมดของฉัน!
นักศึกษา

11

คำตอบอื่น ๆ ให้คำตอบที่กระชับโดยไม่มีข้อเท็จจริงสำคัญ: คุณไม่จำเป็นต้องใช้ combinator จุดคงที่ในภาษาที่ใช้งานจริงในลักษณะที่ซับซ้อนนี้และการทำเช่นนั้นไม่มีจุดประสงค์ในทางปฏิบัติ (ยกเว้น "ดูสิฉันรู้ว่า Y-combinator คือ"). มันเป็นแนวคิดทางทฤษฎีที่สำคัญ แต่มีค่าจริงเล็กน้อย


6

นี่คือการใช้งาน JavaScript ของ Y-Combinator และฟังก์ชัน Factorial (จากบทความของ Douglas Crockford สามารถดูได้ที่: 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
ตลกมาก :) คนหนุ่มสาว (เอ้อ) อาจไม่รู้จักการอ้างอิง
Will Ness

2
ฮ่าฮ่า! อ๋อหนุ่มคนหนึ่ง (ฉัน) ยังคงเข้าใจ ...

ฉันคิดว่ามันเป็นเรื่องจริงและฉันก็ลงเอยที่นี่ youtube.com/watch?v=HyWqxkaQpPwบทความล่าสุดfuturism.com/scientists-made-real-life-flux-capacitor
Saw Thinkar Nay Htoo

ฉันคิดว่าคำตอบนี้อาจสร้างความสับสนให้กับผู้ที่ไม่ใช้ภาษาอังกฤษเป็นพิเศษ บางคนอาจใช้เวลาพอสมควรในการทำความเข้าใจการอ้างสิทธิ์นี้มาก่อน (หรือไม่เคย) ตระหนักว่ามันเป็นการอ้างอิงวัฒนธรรมที่ได้รับความนิยม (ผมชอบมันผมก็จะรู้สึกไม่ดีถ้าผมตอบนี้และพบว่าการเรียนรู้ของคนที่เป็นกำลังใจโดยมัน)
ไมค์

5

ฉันได้เขียนเรียงความของ "คู่มือความโง่เขลา" ให้กับ Y-Combinator ทั้งใน Clojure และ Scheme เพื่อที่จะช่วยตัวเองให้จับมันได้ พวกเขาได้รับอิทธิพลจากเนื้อหาใน "The Little Schemer"

ในรูปแบบ: https://gist.github.com/z5h/238891

หรือ Clojure: https://gist.github.com/z5h/5102747

แบบฝึกหัดทั้งสองนี้มีรหัสสลับกับความคิดเห็นและควรถูกตัดและวางในโปรแกรมแก้ไขที่คุณชื่นชอบ


5

ในฐานะมือใหม่สำหรับผู้รวมตัวกันฉันพบบทความของ Mike Vanier (ขอบคุณ Nicholas Mancuso) ที่เป็นประโยชน์จริงๆ ฉันต้องการเขียนบทสรุปนอกเหนือจากการบันทึกความเข้าใจของฉันหากสามารถช่วยคนอื่นฉันก็จะดีใจมาก

จาก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ต้องใช้ในเส็งเคร็งรุ่นของฟังก์ชั่นแฟกทอ (ซึ่งสามารถคำนวณจนถึงจำนวนx - 1) และผลตอบแทนน้อยเส็งเคร็งรุ่นของปัจจัย (ซึ่งคำนวณจนถึงจำนวนx) เช่นเดียวกับในรูปแบบนี้:

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

ถ้าเราส่งผ่านเส็งเคร็งซ้ำ ๆแฟคทอเรียลเวอร์ชั่นมาalmost-factorial , fในที่สุดเราก็จะได้รับฟังก์ชั่นปัจจัยที่ต้องการของเรา สามารถพิจารณาได้จากที่ไหน:

almost-factorial f = f

แก้ไขจุด

ความจริงที่ว่าalmost-factorial f = fหมายถึงfคือแก้ไขจุดalmost-factorialของฟังก์ชั่น

นี่เป็นวิธีที่น่าสนใจมากที่ได้เห็นความสัมพันธ์ของฟังก์ชั่นด้านบนและมันเป็นช่วงเวลาที่ดีสำหรับฉัน (โปรดอ่านโพสต์ของ Mike ที่จุดแก้ไขหากคุณยังไม่ได้ระบุ)

สามฟังก์ชั่น

ในการพูดคุยทั่วไปเรามีฟังก์ชั่นที่ไม่เกิดซ้ำ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และไปที่ความเข้าใจโพสต์ของ Mike Vainer มีรายละเอียดมากมาย

รูปแบบของ 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)สามารถเป็นฟังก์ชั่นของ arity ใด ๆ , เพื่อให้ง่ายขึ้น, สมมติว่า(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)))

การคำนวณแบบเรียกซ้ำจะสิ้นสุดลงเมื่อ Inner-Most (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'wraps' fnข้างในทุกครั้งที่frใช้ในการคำนวณxมันคือ 'spawns' ('lift'?) a fnและมอบหมายการคำนวณให้กับมันfn(ผ่านตัวมันเองfrและx); ในทางกลับกันfnขึ้นอยู่กับfrและใช้frในการคำนวณผลลัพธ์ของปัญหาที่มีขนาดเล็กลงx-1จากผลการคำนวณของปัญหาที่มีขนาดเล็ก
  • ณ เวลาfrนั้นใช้เพื่อกำหนดfn(เมื่อfnใช้frในการปฏิบัติงาน) ของจริงfrยังไม่ได้กำหนด
  • มันคือfnสิ่งที่กำหนดตรรกะทางธุรกิจที่แท้จริง ขึ้นอยู่กับfn, Yสร้างfr- ฟังก์ชั่นผู้ช่วยในรูปแบบที่เฉพาะเจาะจง - เพื่ออำนวยความสะดวกในการคำนวณสำหรับfnในrecursiveลักษณะ

มันช่วยให้ฉันเข้าใจYวิธีนี้ในตอนนี้หวังว่าจะช่วยได้

BTW ฉันพบหนังสือแนะนำการเขียนโปรแกรมเชิงปฏิบัติการผ่านแลมบ์ดาแคลคูลัสดีมากฉันเป็นส่วนหนึ่งเท่านั้นและข้อเท็จจริงที่ว่าฉันไม่สามารถเอาหัวYอ่านหนังสือเล่มนี้นำฉันมาที่โพสต์นี้


5

ต่อไปนี้เป็นคำตอบของคำถามต้นฉบับที่รวบรวมจาก บทความ (ซึ่งมีมูลค่าการอ่านทั้งหมด) ที่กล่าวถึงในคำตอบของ Nicholas Mancusoรวมถึงคำตอบอื่น ๆ :

Y-combinator คืออะไร

Y-combinator คือ "functional" (หรือฟังก์ชั่นลำดับสูงกว่า - ฟังก์ชั่นที่ทำงานกับฟังก์ชั่นอื่น) ที่รับอาร์กิวเมนต์เดี่ยวซึ่งเป็นฟังก์ชั่นที่ไม่เรียกซ้ำและส่งกลับรุ่นของฟังก์ชันที่ recursive


ค่อนข้างซ้ำ =) แต่มีคำจำกัดความในเชิงลึกมากกว่า:

Combinator - เป็นเพียงนิพจน์แลมบ์ดาที่ไม่มีตัวแปรอิสระ
ตัวแปรอิสระ - เป็นตัวแปรที่ไม่ใช่ตัวแปรที่ถูกผูกไว้
ตัวแปรที่ถูกผูกไว้ - ตัวแปรที่มีอยู่ภายในเนื้อความของแลมบ์ดานิพจน์ที่มีชื่อตัวแปรนั้นเป็นหนึ่งในข้อโต้แย้ง

อีกวิธีในการคิดเกี่ยวกับสิ่งนี้คือ combinator คือนิพจน์แลมบ์ดาซึ่งคุณสามารถแทนที่ชื่อของ combinator ด้วยคำจำกัดความของมันในทุกที่ที่พบและมีทุกอย่างยังคงทำงาน (คุณจะเข้าสู่วงวนไม่สิ้นสุดถ้า combinator มีการอ้างอิงถึงตัวมันเองภายในตัวแลมบ์ดา)

Y-combinator เป็น combinator จุดคงที่

จุดคงที่ของฟังก์ชั่นเป็นองค์ประกอบของโดเมนของฟังก์ชั่นที่แมปกับตัวเองโดยฟังก์ชั่น
กล่าวcคือเป็นจุดคงที่ของฟังก์ชันf(x)ถ้าf(c) = c
นี่หมายถึงf(f(...f(c)...)) = fn(c) = c

ผู้ประสานงานทำงานอย่างไร

ตัวอย่างด้านล่างสมมติว่ามีการพิมพ์ที่รัดกุม + แบบไดนามิก :

Y-combinator สำหรับสันหลังยาว (คำสั่งปกติ):
คำจำกัดความนี้ใช้กับภาษาที่มีสันหลังยาว (เช่น: การรอการอนุมัติ, การโทรตามความต้องการ) - กลยุทธ์การประเมินซึ่งล่าช้าการประเมินผลของนิพจน์จนกว่าจำเป็นต้องใช้ค่า

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

สิ่งนี้หมายความว่าสำหรับฟังก์ชั่นที่กำหนดf(ซึ่งเป็นฟังก์ชั่นที่ไม่ใช่ recursive) ฟังก์ชั่นซ้ำที่สอดคล้องกันสามารถได้รับครั้งแรกโดยการคำนวณλx.f(x x)และจากนั้นใช้การแสดงออกแลมบ์ดานี้เพื่อตัวเอง

Y-combinator ที่เข้มงวด (คำสั่งบังคับใช้):
คำจำกัดความนี้ใช้กับภาษาที่มีการประเมินอย่างเข้มงวด (เช่น: กระตือรือร้น, โลภ) - กลยุทธ์การประเมินที่นิพจน์จะถูกประเมินทันทีที่ถูกผูกกับตัวแปร

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

มันเหมือนกับตัวขี้เกียจในธรรมชาติมันมีตัวλห่อเสริมเพื่อชะลอการประเมินร่างกายของแลมบ์ดา ฉันถามคำถามอื่นเกี่ยวข้องกับหัวข้อนี้บ้าง

สิ่งที่ดีสำหรับ

ขโมยที่ถูกขโมยมาจากคำตอบของ Chris Ammerman : Y-combinator พูดถึงการเรียกซ้ำ, สรุปการดำเนินการและแยกมันออกจากการทำงานจริงของฟังก์ชันที่เป็นปัญหา

ถึงแม้ว่า Y-combinator จะมีแอปพลิเคชั่นที่ใช้งานได้จริง ๆ แล้วมันเป็นแนวคิดทางทฤษฎีความเข้าใจซึ่งจะขยายวิสัยทัศน์โดยรวมของคุณและมีแนวโน้มที่จะเพิ่มทักษะการวิเคราะห์และพัฒนาของคุณ

มันมีประโยชน์ในภาษาเชิงโพรซีเดอร์หรือไม่?

ตามที่ระบุไว้โดย Mike Vanier :เป็นไปได้ที่จะกำหนด Y combinator ในหลายภาษาที่พิมพ์แบบคงที่ แต่ (อย่างน้อยในตัวอย่างที่ฉันเคยเห็น) คำจำกัดความดังกล่าวมักจะต้องการแฮ็คประเภทที่ไม่ชัดเจนเนื่องจาก Y combinator เอง ไม่มีประเภทคงที่ตรงไปตรงมา นั่นเกินขอบเขตของบทความนี้ดังนั้นฉันจะไม่พูดถึงมันเพิ่มเติม

และดังที่ Chris Ammerman กล่าวไว้ : ภาษาส่วนใหญ่มีการพิมพ์แบบคงที่

ดังนั้นตอบคำถามนี้ - ไม่ได้จริงๆ


4

y-combinator ใช้การเรียกซ้ำแบบไม่ระบุชื่อ ดังนั้นแทนที่จะ

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-combinator ใช้งานได้เฉพาะในภาษาที่ใช้เรียกชื่อเท่านั้น หากคุณต้องการใช้สิ่งนี้ในภาษาการโทรตามค่าปกติคุณจะต้องใช้ z-combinator ที่เกี่ยวข้อง (y-combinator จะแยก / infinite-loop)


Y combinator สามารถทำงานร่วมกับการประเมินผลแบบ pass-by-value และ lazy
Quelklef

3

Combinator จุดคงที่ (หรือผู้ประกอบการจุดคงที่) เป็นฟังก์ชั่นการสั่งซื้อที่สูงขึ้นที่คำนวณจุดคงที่ของฟังก์ชั่นอื่น ๆ การดำเนินการนี้มีความเกี่ยวข้องในทฤษฎีภาษาการเขียนโปรแกรมเพราะจะช่วยให้การดำเนินการเรียกซ้ำในรูปแบบของกฎการเขียนซ้ำโดยไม่ได้รับการสนับสนุนอย่างชัดเจนจากโปรแกรมรันไทม์ของภาษา (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-combinator แก้ไขได้


1
คุณแน่ใจหรือว่าไม่ได้สร้างปัญหามากกว่าที่จะแก้ปัญหา?
Noctis Skytower

1
Noctis คุณช่วยชี้แจงคำถามของคุณได้ไหม? คุณกำลังถามว่าแนวคิดของ y-combinator นั้นสร้างปัญหามากกว่าที่จะแก้ไขหรือคุณกำลังพูดถึงเฉพาะที่ฉันเลือกที่จะสาธิตการใช้ JavaScript โดยเฉพาะอย่างยิ่งหรือการใช้งานเฉพาะของฉันหรือคำแนะนำของฉันเพื่อเรียนรู้ด้วยตัวเอง ฉันอธิบาย
zumalifeguard
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.