Một tổ hợp Y là gì? [đóng cửa]


392

Một tổ hợp Y là một khái niệm khoa học máy tính từ phía bên thứ chức năng của thành phố. Hầu hết các lập trình viên không biết nhiều về các tổ hợp, nếu họ thậm chí đã nghe về họ.

  • Một tổ hợp Y là gì?
  • Làm thế nào để tổ hợp làm việc?
  • Chúng giỏi trong việc gì?
  • Chúng có hữu ích trong các ngôn ngữ thủ tục không?

12
Một mẹo nhỏ, nếu bạn đang tìm hiểu về các ngôn ngữ chức năng như tôi, tốt hơn nên để lại các tổ hợp cho đến khi bạn cảm thấy thoải mái với nó, nếu không đó là một con đường dẫn đến sự điên rồ ...
Igor Zevaka

3
Got để mỉm cười với các gravatar của biên tập viên của câu hỏi :) này liên quan liên kết trên blog của Mads Torgensen của
Benjol


1
Tôi đã viết một ý chính ngắn chia sẻ sự hiểu biết của tôi về Y Combinator: gist.github.com/houtianze/b274e4b975a28fe08aee681699c3f7d0 tôi đã giải thích (để hiểu biết của tôi) như thế nào "Y Combinator làm cho hàm đệ quy"
IBIC

1
Làm thế nào là câu hỏi này "quá rộng"?
Rei Miyasaka

Câu trả lời:


201

Nếu bạn đã sẵn sàng cho một thời gian dài đọc, Mike Vanier có một lời giải thích tuyệt vời . Câu chuyện dài, nó cho phép bạn thực hiện đệ quy bằng một ngôn ngữ không nhất thiết phải hỗ trợ nó nguyên bản.


14
Nó hơi nhiều hơn một liên kết mặc dù; nó là một liên kết với một bản tóm tắt rất ngắn gọn . Một bản tóm tắt dài hơn sẽ được đánh giá cao.
Martijn Pieters

2
Nó chỉ là một liên kết NHƯNG nó không thể tốt hơn thế này. Câu trả lời này xứng đáng (add1 phiếu) không có điều kiện trường hợp cơ bản để thoát; aka đệ quy vô hạn.
Yavar

7
@Andre MacFie: Tôi không nhận xét về nỗ lực này, tôi đã nhận xét về chất lượng. Nói chung, chính sách về Stack Overflow là các câu trả lời phải được khép kín, với các liên kết đến nhiều thông tin hơn.
Jørgen Fogh

1
@galdre nói đúng. Nó là một liên kết tuyệt vời, nhưng nó chỉ là một liên kết. Nó cũng đã được đề cập trong 3 câu trả lời khác dưới đây nhưng chỉ là một tài liệu hỗ trợ vì tất cả chúng đều tự giải thích tốt. Câu trả lời này thậm chí không cố gắng trả lời các câu hỏi của OP.
toraritte

290

Bộ kết hợp Y là một "hàm" (một hàm hoạt động trên các hàm khác) cho phép đệ quy, khi bạn không thể tham chiếu đến hàm từ bên trong chính nó. Trong lý thuyết khoa học máy tính, nó khái quát hóa đệ quy , trừu tượng hóa việc thực hiện và do đó tách nó ra khỏi công việc thực tế của hàm đang đề cập. Lợi ích của việc không cần tên thời gian biên dịch cho hàm đệ quy là loại tiền thưởng. =)

Điều này được áp dụng trong các ngôn ngữ hỗ trợ các chức năng lambda . Bản chất dựa trên biểu thức của lambdas thường có nghĩa là chúng không thể tự gọi mình bằng tên. Và làm việc xung quanh điều này bằng cách khai báo biến, tham chiếu đến nó, sau đó gán lambda cho nó, để hoàn thành vòng lặp tự tham chiếu, là dễ vỡ. Biến lambda có thể được sao chép và biến ban đầu được gán lại, phá vỡ tham chiếu tự.

Các tổ hợp Y rất khó thực hiện và thường được sử dụng trong các ngôn ngữ gõ tĩnh (mà ngôn ngữ thủ tục thường sử dụng), bởi vì thông thường các hạn chế nhập yêu cầu số lượng đối số cho hàm được đề cập phải biết trong thời gian biên dịch. Điều này có nghĩa là một bộ kết hợp y phải được viết cho bất kỳ số lượng đối số nào mà người ta cần sử dụng.

Dưới đây là một ví dụ về cách sử dụng và hoạt động của Bộ kết hợp Y, trong C #.

Sử dụng bộ kết hợp Y bao gồm một cách "bất thường" để xây dựng hàm đệ quy. Trước tiên, bạn phải viết hàm của mình dưới dạng một đoạn mã gọi hàm có sẵn, thay vì chính nó:

// Factorial, if func does the same thing as this bit of code...
x == 0 ? 1: x * func(x - 1);

Sau đó, bạn biến nó thành một hàm lấy một hàm để gọi và trả về một hàm làm như vậy. Đây được gọi là một chức năng, bởi vì nó có một chức năng và thực hiện một hoạt động với nó dẫn đến chức năng khác.

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

Bây giờ bạn có một hàm nhận một hàm và trả về một hàm khác trông giống như một giai thừa, nhưng thay vì gọi chính nó, nó gọi đối số được truyền vào hàm bên ngoài. Làm thế nào để bạn thực hiện điều này giai thừa? Truyền chức năng bên trong cho chính nó. Bộ kết hợp Y thực hiện điều đó, bằng cách là một hàm có tên vĩnh viễn, có thể giới thiệu đệ quy.

// 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.
}

Thay vì gọi giai thừa, điều xảy ra là giai thừa gọi là trình tạo giai thừa (được trả về bởi lệnh gọi đệ quy đến Y-Combinator). Và tùy thuộc vào giá trị hiện tại của t, hàm được trả về từ trình tạo sẽ gọi lại trình tạo, với t - 1 hoặc chỉ trả về 1, chấm dứt đệ quy.

Nó phức tạp và khó hiểu, nhưng tất cả đều rung chuyển trong thời gian chạy, và chìa khóa để hoạt động của nó là "thực thi hoãn lại", và phá vỡ đệ quy để mở rộng hai chức năng. F bên trong được truyền dưới dạng đối số , được gọi trong lần lặp tiếp theo, chỉ khi cần thiết .


5
Tại sao oh tại sao bạn phải gọi nó là 'Y' và tham số 'F'! Họ chỉ bị lạc trong các đối số loại!
Brian Henk

3
Trong Haskell, bạn có thể trừu tượng hóa đệ quy với : fix :: (a -> a) -> a, và alần lượt có thể là một hàm có nhiều đối số như bạn muốn. Điều này có nghĩa là gõ tĩnh không thực sự làm cho nó cồng kềnh.
Cốc

12
Theo mô tả của Mike Vanier, định nghĩa của bạn cho Y thực sự không phải là một tổ hợp vì nó là đệ quy. Trong phần "Loại bỏ (hầu hết) đệ quy rõ ràng (phiên bản lười biếng)", anh ta có sơ đồ lười tương đương với mã C # của bạn nhưng giải thích ở điểm 2: "Nó không phải là một tổ hợp, bởi vì Y trong phần thân của định nghĩa là một biến tự do chỉ bị ràng buộc khi định nghĩa hoàn tất ... "Tôi nghĩ điều thú vị về các tổ hợp Y là chúng tạo ra đệ quy bằng cách đánh giá điểm cố định của hàm. Theo cách này, họ không cần đệ quy rõ ràng.
GrantJ

@GrantJ Bạn làm cho một điểm tốt. Đã một vài năm kể từ khi tôi đăng câu trả lời này. Đọc lướt qua bài đăng của Vanier bây giờ tôi thấy rằng tôi đã viết Y, nhưng không phải là Y-Combinator. Tôi sẽ đọc lại bài viết của anh ấy sớm và xem liệu tôi có thể đăng bài chỉnh sửa không. Ruột của tôi đang cảnh báo tôi rằng việc gõ tĩnh nghiêm ngặt của C # có thể ngăn chặn điều đó, nhưng tôi sẽ thấy những gì tôi có thể làm.
Chris Ammerman

1
@WayneBurkett Đó là một thực tế khá phổ biến trong toán học.
YoTengoUnLCD

102

Tôi đã nâng cái này từ http://www.mail-archive.com/boston-pm@mail.pm.org/msg02716.html đó là một lời giải thích tôi đã viết cách đây vài năm.

Tôi sẽ sử dụng JavaScript trong ví dụ này, nhưng nhiều ngôn ngữ khác cũng sẽ hoạt động.

Mục tiêu của chúng tôi là có thể viết hàm đệ quy 1 biến chỉ sử dụng các hàm của 1 biến và không có phép gán, xác định mọi thứ theo tên, v.v. (Tại sao đây là mục tiêu của chúng tôi là một câu hỏi khác, hãy coi đây là thách thức mà chúng tôi 'được đưa ra.) Có vẻ như không thể, phải không? Ví dụ, hãy thực hiện giai thừa.

Bước 1 là nói rằng chúng ta có thể làm điều này một cách dễ dàng nếu chúng ta lừa dối một chút. Sử dụng các hàm của 2 biến và phép gán, ít nhất chúng ta có thể tránh phải sử dụng phép gán để thiết lập đệ quy.

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

Bây giờ hãy xem liệu chúng ta có thể gian lận ít hơn. Đầu tiên, chúng tôi đang sử dụng bài tập, nhưng chúng tôi không cần. Chúng ta chỉ có thể viết nội tuyến X và 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
);

Nhưng chúng tôi đang sử dụng các hàm của 2 biến để có hàm 1 biến. Chúng ta có thể sửa nó không? Vâng, một anh chàng thông minh tên là Haskell Curry có một mánh khóe gọn gàng, nếu bạn có chức năng bậc cao tốt hơn thì bạn chỉ cần chức năng của 1 biến. Bằng chứng là bạn có thể nhận được từ các hàm của 2 (hoặc nhiều hơn trong trường hợp chung) thành 1 biến với một biến đổi văn bản cơ học thuần túy như thế này:

// Original
F = function (i, j) {
  ...
};
F(i,j);

// Transformed
F = function (i) { return function (j) {
  ...
}};
F(i)(j);

trong đó ... vẫn giống hệt nhau. (Thủ thuật này được gọi là "currying" theo người phát minh ra nó. Ngôn ngữ Haskell cũng được đặt tên cho Haskell Curry. Tập tin theo những câu đố vô dụng.) Bây giờ chỉ cần áp dụng chuyển đổi này ở mọi nơi và chúng ta sẽ có phiên bản cuối cùng.

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

Hãy thử nó. alert () trả lại, buộc nó vào một nút, bất cứ điều gì. Mã đó tính toán giai thừa, đệ quy, không sử dụng phép gán, khai báo hoặc hàm của 2 biến. (Nhưng cố gắng theo dõi cách thức hoạt động của nó có khả năng khiến đầu bạn quay cuồng. Và đưa nó, mà không có nguồn gốc, chỉ cần định dạng lại một chút sẽ dẫn đến mã chắc chắn gây trở ngại và nhầm lẫn.)

Bạn có thể thay thế 4 dòng xác định đệ quy giai thừa bằng bất kỳ hàm đệ quy nào khác mà bạn muốn.


Giải thích tốt đẹp. Tại sao bạn viết function (n) { return builder(builder)(n);}thay vì builder(builder)?
v7d8dpo4

@ v7d8dpo4 Bởi vì tôi đã biến một hàm gồm 2 biến thành hàm bậc cao hơn của một biến bằng cách sử dụng currying.
btilly

Đây có phải là lý do chúng ta cần đóng cửa?
TheChetan

1
@TheChetan Đóng cửa cho phép chúng tôi buộc hành vi tùy chỉnh đằng sau một cuộc gọi đến một chức năng ẩn danh. Nó chỉ là một kỹ thuật trừu tượng khác.
btilly

85

Tôi tự hỏi nếu có bất kỳ sử dụng trong nỗ lực để xây dựng này từ đầu. Hãy xem nào. Đây là một nhân tố cơ bản, đệ quy:

function factorial(n) {
    return n == 0 ? 1 : n * factorial(n - 1);
}

Hãy cấu trúc lại và tạo một hàm mới gọi là facttrả về hàm tính toán giai thừa ẩn danh thay vì tự thực hiện phép tính:

function fact() {
    return function(n) {
        return n == 0 ? 1 : n * fact()(n - 1);
    };
}

var factorial = fact();

Điều đó hơi lạ, nhưng không có gì sai với nó. Chúng tôi chỉ tạo ra một chức năng giai thừa mới ở mỗi bước.

Sự đệ quy ở giai đoạn này vẫn còn khá rõ ràng. Các factchức năng cần phải nhận thức được tên riêng của mình. Hãy tham số hóa cuộc gọi đệ quy:

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

Điều đó thật tuyệt, nhưng recurservẫn cần biết tên của chính nó. Chúng ta cũng hãy tham số hóa điều đó:

function recurser(f) {
    return fact(function(x) {
        return f(f)(x);
    });
}

var factorial = recurser(recurser);

Bây giờ, thay vì gọi recurser(recurser)trực tiếp, hãy tạo một hàm bao trả về kết quả của nó:

function Y() {
    return (function(f) {
        return f(f);
    })(recurser);
}

var factorial = Y();

Bây giờ chúng ta có thể thoát khỏi recursertên hoàn toàn; nó chỉ là một đối số cho hàm bên trong của Y, có thể được thay thế bằng chính hàm đó:

function Y() {
    return (function(f) {
        return f(f);
    })(function(f) {
        return fact(function(x) {
            return f(f)(x);
        });
    });
}

var factorial = Y();

Tên bên ngoài duy nhất vẫn được tham chiếu là fact, nhưng bây giờ cũng phải rõ ràng rằng điều đó cũng dễ dàng được tham số hóa, tạo ra giải pháp hoàn chỉnh, chung chung:

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

Một lời giải thích tương tự trong JavaScript: igstan.ro/posts/ Lần
Pops

1
Bạn mất tôi khi bạn giới thiệu chức năng recurser. Không phải là ý tưởng nhỏ nhất những gì nó đang làm, hoặc tại sao.
Mörre

2
Chúng tôi đang cố gắng xây dựng một giải pháp đệ quy chung cho các hàm không đệ quy rõ ràng. Các recurserchức năng là bước đầu tiên hướng tới mục tiêu này, bởi vì nó mang lại cho chúng ta một phiên bản đệ quy của factmà không bao giờ tham chiếu riêng của mình theo tên.
Wayne

@WayneBurkett, tôi có thể viết lại tổ hợp Y như thế này không : function Y(recurse) { return recurse(recurse); } let factorial = Y(creator => value => { return value == 0 ? 1 : value * creator(creator)(value - 1); });. Và đây là cách tôi tiêu hóa nó (không chắc là nó có đúng không): Bằng cách không tham chiếu rõ ràng chức năng (không được phép như là một tổ hợp ), chúng ta có thể sử dụng hai hàm được áp dụng một phần (hàm tạo và hàm tính toán), với mà chúng ta có thể tạo các hàm lambda / ẩn danh để đạt được đệ quy mà không cần tên cho hàm tính toán?
neevek

50

Hầu hết các câu trả lời ở trên mô tả Y-combinator gì nhưng không phải là nó dùng để làm gì .

Combinators điểm cố định được sử dụng để chứng minh rằng phép tính lambdaTuring hoàn tất . Đây là một kết quả rất quan trọng trong lý thuyết tính toán và cung cấp một nền tảng lý thuyết cho lập trình chức năng .

Nghiên cứu các tổ hợp điểm cố định cũng đã giúp tôi thực sự hiểu lập trình chức năng. Tôi chưa bao giờ tìm thấy bất kỳ sử dụng cho họ trong lập trình thực tế mặc dù.


24

trình kết hợp y trong 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

Biên tập : Tôi học được rất nhiều từ việc xem mã, nhưng cái này hơi khó nuốt mà không có chút nền tảng nào - xin lỗi về điều đó. Với một số kiến ​​thức chung được trình bày bởi các câu trả lời khác, bạn có thể bắt đầu chọn ra những gì đang xảy ra.

Hàm Y là "tổ hợp y". Bây giờ hãy xem var factorialdòng Y được sử dụng. Lưu ý rằng bạn truyền một hàm cho nó có một tham số (trong ví dụ này recurse), cũng được sử dụng sau này trong hàm bên trong. Tên tham số về cơ bản trở thành tên của hàm bên trong cho phép nó thực hiện cuộc gọi đệ quy (vì nó sử dụng recurse()trong định nghĩa của nó.) Bộ kết hợp y thực hiện phép thuật liên kết hàm bên trong ẩn danh khác với tên tham số của hàm được truyền cho Y.

Để biết giải thích đầy đủ về cách Y thực hiện phép thuật, hãy xem bài viết được liên kết (không phải bởi tôi btw.)


6
Javascript không cần trình kết hợp Y để thực hiện đệ quy ẩn danh vì bạn có thể truy cập hàm hiện tại bằng argument.callee (xem en.wikipedia.org/wiki/
trộm

6
arguments.calleekhông được phép trong Chế độ nghiêm ngặt: developer.mozilla.org/en/JavaScript/ từ
dave1010

2
Bạn vẫn có thể đặt tên cho bất kỳ hàm nào và nếu đó là biểu thức hàm thì tên đó chỉ được biết bên trong hàm đó. (function fact(n){ return n <= 1? 1 : n * fact(n-1); })(5)
Esailija

1
ngoại trừ trong IE. kangax.github.io/nfe
VoronoiPotato

18

Đối với các lập trình viên chưa gặp phải lập trình chức năng chuyên sâu và không quan tâm để bắt đầu ngay bây giờ, nhưng hơi tò mò:

Bộ kết hợp Y là một công thức cho phép bạn thực hiện đệ quy trong tình huống các hàm không thể có tên nhưng có thể được chuyển qua làm đối số, được sử dụng làm giá trị trả về và được xác định trong các hàm khác.

Nó hoạt động bằng cách truyền hàm cho chính nó như là một đối số, vì vậy nó có thể tự gọi nó.

Đó là một phần của phép tính lambda, thực sự là toán học nhưng thực sự là ngôn ngữ lập trình, và khá cơ bản đối với khoa học máy tính và đặc biệt là lập trình chức năng.

Giá trị thực tế hàng ngày của bộ kết hợp Y bị giới hạn, vì các ngôn ngữ lập trình có xu hướng cho phép bạn đặt tên hàm.

Trong trường hợp bạn cần xác định nó trong đội hình cảnh sát, nó sẽ trông như thế này:

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

Bạn thường có thể phát hiện ra nó vì sự lặp đi lặp lại (λx.f (x x)) .

Các λbiểu tượng là chữ lambda của Hy Lạp, đặt tên cho lambda tính toán của nó, và có rất nhiều (λx.t)thuật ngữ về phong cách bởi vì đó là những gì tính toán lambda trông giống như vậy.


đây sẽ là câu trả lời được chấp nhận BTW, với U x = x x, Y = U . (. U)(lạm dụng ký hiệu giống Haskell). IOW, với các tổ hợp thích hợp , Y = BU(CBU). Như vậy , Yf = U (f . U) = (f . U) (f . U) = f (U (f . U)) = f ((f . U) (f . U)).
Will Ness

13

Đệ quy ẩn danh

Một tổ hợp điểm cố định là một hàm bậc cao hơn fixtheo định nghĩa thỏa mãn tính tương đương

forall f.  fix f  =  f (fix f)

fix fđại diện cho một giải pháp xcho phương trình điểm cố định

               x  =  f x

Giai thừa của một số tự nhiên có thể được chứng minh bằng

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

Sử dụng fix, bằng chứng xây dựng tùy ý trên các hàm tổng quát /-đệ quy có thể được lấy mà không có tính tự tham chiếu bất đối xứng.

fact n = (fix fact') n

Ở đâu

fact' rec n = if n == 0
                then 1
                else n * rec (n - 1)

như vậy mà

   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

Bằng chứng chính thức này

fact 3  =  6

phương pháp sử dụng tương đương tổ hợp điểm cố định để viết lại

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

Giải tích Lambda

Các untyped phép tính lambda hình thức bao gồm trong một ngữ pháp ngữ cảnh miễn phí

E ::= v        Variable
   |  λ v. E   Abstraction
   |  E E      Application

trong đó vphạm vi trên các biến, cùng với các quy tắc giảm betaeta

(λ x. B) E  ->  B[x := E]                                 Beta
  λ x. E x  ->  E          if x doesn’t occur free in E   Eta

Giảm beta thay thế tất cả các lần xuất hiện tự do của biến xtrong phần trừu tượng (hàm chức năng) Bbằng biểu thức (đối số trực tuyến) E. Giảm Eta loại bỏ sự trừu tượng dư thừa. Nó đôi khi được bỏ qua từ chủ nghĩa hình thức. Một biểu thức không thể sửa chữa, mà không áp dụng quy tắc giảm nào, ở dạng bình thường hoặc chính tắc .

λ x y. E

là tốc ký cho

λ x. λ y. E

(đa dạng trừu tượng),

E F G

là tốc ký cho

(E F) G

(ứng dụng kết hợp trái),

λ x. x

λ y. y

alpha-tương đương .

Trừu tượng hóa và ứng dụng là hai nguyên thủy duy nhất của ngôn ngữ Tiếng Anh của tính toán lambda, nhưng chúng cho phép mã hóa các dữ liệu và hoạt động phức tạp tùy ý.

Các chữ số của Giáo hội là một mã hóa của các số tự nhiên tương tự như các tiên số 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
    . . .

Một bằng chứng chính thức rằng

1 + 2  =  3

sử dụng quy tắc viết lại giảm beta:

   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

Kết hợp

Trong phép tính lambda, tổ hợp là trừu tượng không chứa biến miễn phí. Đơn giản nhất : I, trình kết hợp danh tính

λ x. x

đẳng cấu với chức năng nhận dạng

id x = x

Các tổ hợp như vậy là các toán tử nguyên thủy của phép tính tổ hợp như hệ thống SKI.

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

Beta giảm không được bình thường hóa mạnh mẽ ; không phải tất cả các biểu thức có thể rút gọn, mà redexes, chuyển sang dạng bình thường dưới dạng giảm beta. Một ví dụ đơn giản là ứng dụng khác nhau của ωtổ hợp omega

λ x. x x

với chính nó:

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

Việc giảm các biểu hiện phụ ngoài cùng bên trái (các đầu của người Hồi giáo) được ưu tiên. Thứ tự áp dụng bình thường hóa các đối số trước khi thay thế, thứ tự bình thường không. Hai chiến lược tương tự như đánh giá háo hức, ví dụ C và đánh giá lười biếng, ví dụ Haskell.

   K          (I a)        (ω ω)
=  (λ k l. k) ((λ i. i) a) ((λ x. x x) (λ y. y y))

phân kỳ theo giảm beta đơn đặt hàng háo hức

=  (λ 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))
. . .
=  _|_

kể từ trong ngữ nghĩa nghiêm ngặt

forall f.  f _|_  =  _|_

nhưng hội tụ dưới mức giảm beta đơn hàng bình thường

=  (λ l. ((λ i. i) a)) ((λ x. x x) (λ y. y y))
=  (λ l. a) ((λ x. x x) (λ y. y y))
=  a

Nếu một biểu thức có dạng bình thường, giảm beta theo thứ tự thông thường sẽ tìm thấy nó.

Y

Các thuộc tính cần thiết của Y tổ hợp điểm cố định

λ f. (λ x. f (x x)) (λ x. f (x x))

được đưa ra bởi

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

Sự tương đương

Y g  =  g (Y g)

đẳng cấu

fix f  =  f (fix f)

Phép tính lambda chưa được đánh dấu có thể mã hóa các bằng chứng xây dựng tùy ý trên các hàm tổng quát / μ-đệ quy.

 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

(Nhân chậm, hợp lưu)

Đối với phép tính lambda chưa được đánh dấu của Churchian, đã được chứng minh là tồn tại vô số đệ quy vô số các tổ hợp điểm cố định bên cạnh đó 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))
  . . .

Việc giảm beta theo thứ tự thông thường làm cho lambda không được kiểm duyệt tính toán thành một hệ thống viết lại hoàn chỉnh Turing.

Trong Haskell, bộ kết hợp điểm cố định có thể được thực hiện một cách thanh lịch

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

Sự lười biếng của Haskell bình thường hóa đến mức hoàn thiện trước khi tất cả các biểu hiện phụ được đánh giá.

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


4
Mặc dù tôi đánh giá cao sự kỹ lưỡng của câu trả lời, nhưng không có cách nào tiếp cận được với một lập trình viên có ít nền tảng toán học chính thức sau khi ngắt dòng đầu tiên.
Jared Smith

4
@ jared-smith Câu trả lời có nghĩa là kể một câu chuyện bổ sung của Wonkaian về các khái niệm CS / toán học đằng sau tổ hợp Y. Tôi nghĩ rằng, có lẽ, sự tương tự tốt nhất có thể với các khái niệm quen thuộc đã được rút ra bởi những người trả lời khác. Cá nhân, tôi luôn thích được đối mặt với nguồn gốc thực sự, sự mới lạ triệt để của một ý tưởng, qua một sự tương tự tốt đẹp. Tôi thấy hầu hết các tương tự rộng rãi không phù hợp và khó hiểu.

1
Xin chào, tổ hợp danh tính λ x . x, hôm nay bạn thế nào?
MaiaVictor

Tôi thích câu trả lời này nhất . Nó chỉ xóa tất cả các câu hỏi của tôi!
Sinh viên

11

Các câu trả lời khác cung cấp câu trả lời khá súc tích cho vấn đề này, mà không có một thực tế quan trọng nào: Bạn không cần phải thực hiện trình kết hợp điểm cố định bằng bất kỳ ngôn ngữ thực tế nào theo cách phức tạp này và làm như vậy không phục vụ mục đích thực tế nào (ngoại trừ "nhìn, tôi biết Y-combinator là gì Là"). Đó là khái niệm lý thuyết quan trọng, nhưng ít có giá trị thực tiễn.


6

Đây là một triển khai JavaScript của Y-Combinator và hàm Factorial (từ bài viết của Douglas Crockford, có tại: 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

Bộ kết hợp Y là tên gọi khác của tụ điện thông lượng.


4
rất buồn cười :) những người trẻ (er) có thể không nhận ra tài liệu tham khảo mặc dù.
Will Ness

2
haha Đúng, người trẻ (tôi) vẫn có thể hiểu ...

Tôi nghĩ đó là sự thật và tôi đã kết thúc ở đây. youtube.com/watch?v=HyWqxkaQpPw Điều gần đây futurism.com/scientists-made-real-life-flux-capacitor
Saw Thinkar Nay Htoo

Tôi nghĩ rằng câu trả lời này có thể đặc biệt khó hiểu đối với những người không nói tiếng Anh. Người ta có thể dành khá nhiều thời gian để hiểu về tuyên bố này trước khi (hoặc không bao giờ) nhận ra rằng đó là một tài liệu tham khảo văn hóa phổ biến hài hước. (Tôi thích nó, tôi sẽ cảm thấy tồi tệ nếu tôi trả lời điều này và phát hiện ra rằng ai đó đang học bị nó làm nản lòng)
mike

5

Tôi đã viết một loại "hướng dẫn ngu ngốc" cho Y-Combinator trong cả Clojure và Scheme để giúp bản thân hiểu rõ hơn về nó. Họ bị ảnh hưởng bởi vật chất trong "The Little Schemer"

Trong sơ đồ: https://gist.github.com/z5h/238891

hoặc Clojure: https://gist.github.com/z5h/5102747

Cả hai hướng dẫn đều là mã xen kẽ với các bình luận và nên được cắt & dán vào trình soạn thảo yêu thích của bạn.


5

Là một người mới tham gia tổ hợp, tôi tìm thấy bài viết của Mike Vanier (cảm ơn Nicholas Mancuso) thực sự hữu ích. Tôi muốn viết một bản tóm tắt, bên cạnh việc ghi lại sự hiểu biết của tôi, nếu nó có thể giúp ích cho một số người khác, tôi sẽ rất vui mừng.

Từ Crappy đến Ít Crappy

Lấy giai thừa làm ví dụ, chúng tôi sử dụng almost-factorialhàm sau để tính giai thừa của số x:

def almost-factorial f x = if iszero x
                           then 1
                           else * x (f (- x 1))

Trong mã giả ở trên, almost-factorialcó chức năng fvà số x( almost-factorialđược xử lý, vì vậy nó có thể được xem là nhận chức năng fvà trả về hàm 1-arity).

Khi almost-factorialtính giai thừa cho x, nó ủy nhiệm tính toán giai thừa cho x - 1hàm fvà tích lũy kết quả đó vớix (trong trường hợp này, nó nhân kết quả của (x - 1) với x).

Nó có thể được coi là almost-factorialmất trong một crappy phiên bản của chức năng thừa (mà chỉ có thể tính toán đến số x - 1) và trả về một ít crappy phiên bản của giai thừa (mà tính toán đến số x). Như trong hình thức này:

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

Nếu chúng ta liên tục chuyển phiên bản yếu tố ít ỏi hơnalmost-factorial , cuối cùng chúng ta sẽ có được chức năng giai thừa mong muốn f. Nơi nó có thể được coi là:

almost-factorial f = f

Điểm cố định

Thực tế almost-factorial f = fcó nghĩa flà điểm cố định của chức năngalmost-factorial .

Đây là một cách thực sự thú vị để xem các mối quan hệ của các chức năng trên và đó là một khoảnh khắc aha đối với tôi. (vui lòng đọc bài viết của Mike về điểm sửa chữa nếu bạn chưa có)

Ba chức năng

Suy rộng ra, chúng ta có một không đệ quy chức năng fn(như chúng tôi gần như thừa), chúng tôi có nó sửa chữa điểm chức năng fr(như f của chúng tôi), sau đó điều gì Ylàm là khi bạn đưa ra Y fn, Ytrả về chức năng sửa chữa điểm của fn.

Vì vậy, tóm lại (đơn giản hóa bằng cách giả sử frchỉ mất một tham số; xsuy biến thành x - 1, x - 2... trong đệ quy):

  • Chúng tôi xác định các tính toán cốt lõifn: def fn fr x = ...accumulate x with result from (fr (- x 1)), đây là chức năng gần như hữu ích - mặc dù chúng tôi không thể sử dụng fntrực tiếp x, nhưng nó sẽ rất hữu ích trong thời gian ngắn. Không đệ quy này fnsử dụng một hàm frđể tính kết quả của nó
  • fn fr = fr, frLà việc sửa chữa điểm của fn, frhữu ích funciton, chúng ta có thể sử dụng frtrên xđể có được kết quả của chúng tôi
  • Y fn = fr, YTrả về sửa chữa điểm của một hàm, Y biến chúng tôi gần như hữu ích chức năng fnvào hữu ích fr

Xuất phát Y(không bao gồm)

Tôi sẽ bỏ qua việc phái sinh Yvà đi đến sự hiểu biết Y. Bài đăng của Mike Vainer có rất nhiều chi tiết.

Hình thức của Y

Yđược định nghĩa là (theo định dạng lambda tính toán ):

Y f = λs.(f (s s)) λs.(f (s s))

Nếu chúng ta thay thế biến sở bên trái của hàm, chúng ta sẽ nhận được

Y f = λs.(f (s s)) λs.(f (s s))
=> f (λs.(f (s s)) λs.(f (s s)))
=> f (Y f)

Vì vậy, thực sự, kết quả (Y f)là điểm sửa chữa của f.

Tại sao (Y f)làm việc?

Tùy thuộc vào chữ ký của f, (Y f)có thể là một hàm của bất kỳ arity nào, để đơn giản hóa, giả sử (Y f)chỉ lấy một tham số, như hàm giai thừa của chúng ta.

def fn fr x = accumulate x (fr (- x 1))

kể từ đó fn fr = fr, chúng tôi tiếp tục

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

phép tính đệ quy chấm dứt khi phần lớn bên trong (fn fr 1)là trường hợp cơ sở và fnkhông sử dụng frtrong phép tính.

Nhìn Ylại:

fr = Y fn = λs.(fn (s s)) λs.(fn (s s))
=> fn (λs.(fn (s s)) λs.(fn (s s)))

Vì thế

fr x = Y fn x = fn (λs.(fn (s s)) λs.(fn (s s))) x

Đối với tôi, phần kỳ diệu của thiết lập này là:

  • fnfrphụ thuộc lẫn nhau: fr'kết thúc' fnbên trong, mỗi lần frđược sử dụng để tính toán x, nó 'sinh ra' ('thang máy'?) fnvà ủy thác phép tính cho điều đó fn(tự đi qua frx); mặt khác, fnphụ thuộc frvà sử dụng frđể tính kết quả của một vấn đề nhỏ hơn x-1.
  • Tại thời điểm frđược sử dụng để xác định fn(khi fnsử dụng frtrong các hoạt động của nó), thực tế frchưa được xác định.
  • Đó là fnđịnh nghĩa logic kinh doanh thực sự. Dựa trên fn, Ytạo ra fr- một hàm helper trong một hình thức cụ thể - để thuận lợi cho việc tính toán cho fnmột đệ quy cách.

Nó giúp tôi hiểu Ytheo cách này vào lúc này, hy vọng nó sẽ giúp.

BTW, tôi cũng thấy cuốn sách Giới thiệu về lập trình chức năng thông qua Lambda Tính toán rất hay, tôi chỉ là một phần của nó và thực tế là tôi không thể hiểu được Ytrong cuốn sách dẫn tôi đến bài viết này.


5

Dưới đây là câu trả lời cho các câu hỏi ban đầu , được tổng hợp từ bài viết (là TOTALY đáng đọc) được đề cập trong câu trả lời của Nicholas Mancuso , cũng như các câu trả lời khác:

Một tổ hợp Y là gì?

Một tổ hợp Y là một "hàm" (hoặc hàm bậc cao hơn - một hàm hoạt động trên các hàm khác) có một đối số duy nhất, là một hàm không đệ quy và trả về một phiên bản của hàm đệ quy.


Hơi đệ quy =), nhưng định nghĩa sâu hơn:

Một tổ hợp - chỉ là một biểu thức lambda không có biến miễn phí.
Biến tự do - là biến không phải là biến bị ràng buộc.
Biến giới hạn - biến được chứa bên trong phần thân của biểu thức lambda có tên biến đó là một trong các đối số của nó.

Một cách khác để suy nghĩ về điều này là bộ kết hợp là một biểu thức lambda, trong đó bạn có thể thay thế tên của bộ kết hợp bằng định nghĩa của nó ở mọi nơi nó được tìm thấy và mọi thứ vẫn hoạt động (bạn sẽ đi vào một vòng lặp vô hạn nếu bộ kết hợp sẽ chứa tham chiếu đến chính nó, bên trong cơ thể lambda).

Y-combinator là một tổ hợp điểm cố định.

Điểm cố định của hàm là một thành phần của miền của hàm được ánh xạ tới chính nó bởi hàm.
Nghĩa là, clà một điểm cố định của hàmf(x) nếu f(c) = c
Điều này có nghĩa làf(f(...f(c)...)) = fn(c) = c

Làm thế nào để tổ hợp làm việc?

Ví dụ dưới đây giả định kiểu gõ mạnh + động :

Người lười biếng (theo thứ tự bình thường) Y-combinator:
Định nghĩa này áp dụng cho các ngôn ngữ với đánh giá lười biếng (cũng: trì hoãn, gọi theo nhu cầu) - chiến lược đánh giá trì hoãn việc đánh giá biểu thức cho đến khi cần giá trị của nó.

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

Điều này có nghĩa là gì, đối với một chức năng nhất định f (là hàm không đệ quy), hàm đệ quy tương ứng có thể được lấy trước bằng cách tính toán λx.f(x x), sau đó áp dụng biểu thức lambda này cho chính nó.

Kết hợp Y nghiêm ngặt (theo thứ tự áp dụng):
Định nghĩa này áp dụng cho các ngôn ngữ có đánh giá nghiêm ngặt (cũng: háo hức, tham lam) - chiến lược đánh giá trong đó một biểu thức được đánh giá ngay khi nó bị ràng buộc với một biến.

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

Nó giống như một người lười biếng trong bản chất của nó, nó chỉ có thêm một λgói để trì hoãn việc đánh giá cơ thể của lambda. tôi đã hỏi một câu hỏi khác , phần nào liên quan đến chủ đề này.

Chúng giỏi trong việc gì?

Bị đánh cắp mượn từ câu trả lời của Chris Ammerman : Y-combinator tổng quát hóa đệ quy, trừu tượng hóa việc thực hiện nó, và do đó tách nó ra khỏi công việc thực tế của hàm đang đề cập.

Mặc dù, Y-combinator có một số ứng dụng thực tế, nó chủ yếu là một khái niệm lý thuyết, hiểu về nó sẽ mở rộng tầm nhìn tổng thể của bạn và sẽ, có khả năng, làm tăng kỹ năng phân tích và phát triển của bạn.

Chúng có hữu ích trong các ngôn ngữ thủ tục không?

Như Mike Vanier đã tuyên bố : có thể định nghĩa trình kết hợp Y bằng nhiều ngôn ngữ được nhập tĩnh, nhưng (ít nhất là trong các ví dụ tôi đã thấy) các định nghĩa như vậy thường yêu cầu một số loại hack không rõ ràng, vì bản thân trình kết hợp Y không ' t có kiểu tĩnh đơn giản. Điều đó nằm ngoài phạm vi của bài viết này, vì vậy tôi sẽ không đề cập thêm

Và như Chris Ammerman đã đề cập : hầu hết các ngôn ngữ thủ tục đều có kiểu gõ tĩnh.

Vì vậy, trả lời cho điều này - không thực sự.


4

Bộ kết hợp y thực hiện đệ quy ẩn danh. Vì vậy, thay vì

function fib( n ){ if( n<=1 ) return n; else return fib(n-1)+fib(n-2) }

bạn có thể làm

function ( fib, n ){ if( n<=1 ) return n; else return fib(n-1)+fib(n-2) }

tất nhiên, bộ kết hợp y chỉ hoạt động trong các ngôn ngữ gọi theo tên. Nếu bạn muốn sử dụng điều này trong bất kỳ ngôn ngữ gọi theo giá trị thông thường nào, thì bạn sẽ cần trình kết hợp z có liên quan (y-combinator sẽ phân kỳ / vòng lặp vô hạn).


Bộ kết hợp Y có thể làm việc với đánh giá thụ động và lười biếng.
Quelklef

3

Một tổ hợp điểm cố định (hoặc toán tử điểm cố định) là một hàm bậc cao hơn, tính toán một điểm cố định của các hàm khác. Hoạt động này có liên quan trong lý thuyết ngôn ngữ lập trình vì nó cho phép thực hiện đệ quy dưới dạng quy tắc viết lại, mà không cần sự hỗ trợ rõ ràng từ công cụ thời gian chạy của ngôn ngữ. (src Wikipedia)


3

Toán tử này có thể đơn giản hóa cuộc sống của bạn:

var Y = function(f) {
    return (function(g) {
        return g(g);
    })(function(h) {
        return function() {
            return f.apply(h(h), arguments);
        };
    });
};

Sau đó, bạn tránh các chức năng bổ sung:

var fac = Y(function(n) {
    return n == 0 ? 1 : n * this(n - 1);
});

Cuối cùng, bạn gọi fac(5).


0

Tôi nghĩ cách tốt nhất để trả lời điều này là chọn một ngôn ngữ, như 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));
    }
}

Bây giờ viết lại nó để nó không sử dụng tên của hàm bên trong hàm, nhưng vẫn gọi nó một cách đệ quy.

Nơi duy nhất tên chức năng factorialnên được nhìn thấy là tại trang web cuộc gọi.

Gợi ý: bạn không thể sử dụng tên của các hàm, nhưng bạn có thể sử dụng tên của các tham số.

Làm việc có vấn đề. Đừng nhìn nó. Một khi bạn giải quyết nó, bạn sẽ hiểu vấn đề của bộ kết hợp y giải quyết vấn đề gì.


1
Bạn có chắc chắn rằng nó không tạo ra nhiều vấn đề hơn nó giải quyết?
Noctis Skytower

1
Noctis, bạn có thể làm rõ câu hỏi của bạn? Bạn đang hỏi liệu khái niệm về một bộ kết hợp y có tạo ra nhiều vấn đề hơn nó không, hay bạn đang nói về cụ thể mà tôi đã chọn để chứng minh bằng cách sử dụng JavaScript nói riêng, hoặc việc triển khai cụ thể của tôi hoặc đề xuất của tôi để tìm hiểu nó bằng cách tự khám phá nó Tôi mô tả?
bảo vệ zumalifif
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.