Yコンビネーターとは何ですか?[閉まっている]


392

Yコンビネーターは、物事の「機能」面からのコンピューターサイエンスコンセプトです。ほとんどのプログラマーは、コンビネーターについて聞いたことさえあれば、コンビネーターについてまったく知りません。

  • Yコンビネーターとは何ですか?
  • コンビネーターはどのように機能しますか?
  • 彼らは何のために良いのですか?
  • それらは手続き型言語で役立ちますか?

12
ヒントのように、あなたが私のような関数型言語について学んでいる場合は、それに慣れるまでコンビネーターを残しておくとよいでしょう。そうでなければ、それは狂気への道です...
Igor Zevaka

3
この質問の編集者の墓場で
Benjol


1
私は、Y Combinatorについての私の理解を共有する短い要点を書きました:gist.github.com/houtianze/b274e4b975a28fe08aee681699c3f7d0「Y Combinatorが再帰的な機能を作る」方法を説明しました
ibic

1
この質問は「広すぎる」のでしょうか。
宮坂零

回答:


201

あなたが長い読書の準備ができているなら、マイクバニエは持っています素晴らしい説明をし。要するに、再帰を必ずしもネイティブでサポートしているわけではない言語で実装することができます。


14
ただし、リンクよりも少し多いです。それは非常に短い要約とのリンクです。より長い要約をいただければ幸いです。
Martijn Pieters

2
それは単なるリンクですが、これより良くなることはできません。この回答は、終了するための基本的なケースの条件なしで(add1票)に値します。別名無限再帰。
Yavar 2015年

7
@Andre MacFie:努力についてはコメントせず、品質についてコメントしました。一般に、Stack Overflowのポリシーでは、回答は自己完結型であり、詳細情報へのリンクが必要です。
ヨルゲンFogh

1
@galdreは正しいです。すばらしいリンクですが、単なるリンクです。また、以下の3つの他の回答でも言及されていますが、それらはすべて独自の優れた説明であるため、サポートドキュメントとしてのみです。この答えは、OPの質問にも答えようとはしません。
toraritte 2018年

290

Yコンビネータは、関数内から関数を参照できない場合に再帰を可能にする「関数」(他の関数で動作する関数)です。コンピュータサイエンス理論では、それは再帰を一般化し、その実装を抽象化し、それにより問題の関数の実際の作業からそれを分離します。再帰関数にコンパイル時の名前を必要としない利点は、一種のボーナスです。=)

これは、ラムダ関数をサポートする言語に適用されます表現ラムダベースの性質は、通常、名前で自分自身を参照できないことを意味します。そして、変数を宣言して参照し、次にラムダを代入して自己参照ループを完了するという方法でこれに対処することは、脆弱です。ラムダ変数をコピーすると、元の変数が再割り当てされ、自己参照が壊れます。

Y型コンビネーターは、静的型付き言語(手続き型言語がそうであることが多い)での実装や使用が面倒です。通常、型の制限により、問題の関数の引数の数をコンパイル時に知る必要があるためです。これは、使用する必要のある引数カウントに対してy結合子を記述する必要があることを意味します。

以下は、C#でのYコンビネーターの使用方法と動作の例です。

Yコンビネーターの使用には、再帰関数を作成する「通常とは異なる」方法が含まれます。最初に、関数をそれ自体ではなく、既存の関数を呼び出すコードの一部として記述する必要があります。

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

次に、それを関数を呼び出して呼び出す関数に変換し、その関数を返します。これは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を返し、再帰を終了します。

それは複雑で不可解ですが、実行時にすべてが揺さぶられ、その動作の鍵は「据え置き実行」と、2つの関数にまたがる再帰の分割です。内部Fは引数として渡され、必要な場合のみ、次の反復で呼び出されます。


5
なぜああ、なぜそれを 'Y'とパラメータ 'F'と呼ばなければならなかったのですか?それらは型引数で失われるだけです!
ブライアンヘンク2011

3
Haskellでは、次のように再帰を抽象化できますfix :: (a -> a) -> a。そして、acanは必要なだけの引数の関数にすることができます。これは、静的型付けが実際にこれを面倒にすることはないことを意味します。
ピーカー、2011

12
Mike Vanierの説明によると、Yの定義は再帰的であるため、実際にコンビネーターではありません。「(ほとんどの)明示的な再帰(遅延バージョン)の除去」では、C#コードに相当する遅延スキームがありますが、ポイント2で説明されています。定義が完了して初めてバインドされます...」Y結合器のすばらしい点は、関数の固定小数点を評価することにより再帰を生成することです。このようにして、明示的な再帰は必要ありません。
GrantJ 2011

@GrantJあなたは良い点を作ります。この回答を投稿してから2年になります。バニエ氏の投稿をざっと見ると、私はYを書いたことがわかりますが、Yコンビネーターではありません。彼の投稿をすぐにもう一度読んで、修正を投稿できるかどうかを確認します。私の直感は、C#の厳密な静的型付けが最終的にそれを防ぐ可能性があることを警告していますが、私は何ができるかを見ていきます。
Chris Ammerman、2011

1
@WayneBurkett数学ではかなり一般的な方法です。
YoTengoUnLCD 2016年

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

ぜひお試しください。戻って、それをボタンに結び付けるalert()。このコードは、2つの変数の代入、宣言、関数を使用せずに、階乗を再帰的に計算します。(しかし、それがどのように機能するかを追跡しようとすると、頭が回転する可能性があります。派生せずにそれを渡すと、わずかに再フォーマットするだけで、コードが邪魔になり混乱することになります。)

階乗を再帰的に定義する4行を、必要な他の再帰関数に置き換えることができます。


いい説明。function (n) { return builder(builder)(n);}代わりになぜ書いたのbuilder(builder)ですか?
v7d8dpo4 2016

@ v7d8dpo4カリーを使用して、2つの変数の関数を1つの変数の高次関数に変換していたためです。
btilly 2016

これが閉鎖が必要な理由ですか?
TheChetan 2017年

1
@TheChetan Closuresを使用すると、呼び出しの背後でカスタマイズされた動作を匿名関数に結び付けることができます。これは、もう1つの抽象化手法です。
-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/...
ポップ

1
その機能を導入したときにあなたは私を失いましたrecurser。それが何をしているのか、その理由は少しもわかりません。
Mörre

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); });。そして、これは私がそれを消化する方法です(それが正しいかどうかはわかりません):関数を明示的に参照しないことで(コンビネータとしては許可されません)、2つの部分的に適用/カリー化された関数(作成者関数と計算関数)を使用できます計算関数の名前を必要とせずに再帰を実現するラムダ/無名関数を作成できるものはどれですか。
neevek 2018年

50

上記の回答のほとんどは、Yコンビネータは何を説明され、それが何であるかではなく、ために

固定小数点コンビネータは、ラムダ計算完全に処理されていることを示すために使用されます。これは計算理論において非常に重要な結果であり、関数型プログラミングの理論的基盤を提供します

固定小数点コンビネーターを研究することも、関数型プログラミングを本当に理解するのに役立ちました。私は実際のプログラミングでそれらの使用を見つけたことはありません。


24

JavaScriptの y 結合子

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 factorialYが使用されている行を見てみましょう。recurse内部の関数でも後で使用されるパラメーター(この例では)を持つ関数を渡したことに注意してください。パラメーター名は基本的に、内部関数の名前になり、再帰呼び出しを実行できるようになります(recurse()その定義で使用されるため)。y-combinatorは、匿名の内部関数を、渡された関数のパラメーター名に関連付ける魔法を実行しますY.

Yがどのように魔法を使うかについての完全な説明については、リンクされた記事をチェックしてください(私ではありません)。


6
arguments.callee(en.wikipedia.org/wiki/…を参照)で現在の関数にアクセスできるため、JavaScriptは匿名再帰を実行するためにYコンビ
ネーターを

6
arguments.callee:strictモードでは許可されていませんdeveloper.mozilla.org/en/JavaScript/...
dave1010

2
任意の関数に名前を付けることができます。関数式の場合、その名前は関数自体の内部でのみ認識されます。(function fact(n){ return n <= 1? 1 : n * fact(n-1); })(5)
エサイリヤ2012


18

関数型プログラミングに詳しくなく、今は始めたくないが、少し興味があるプログラマー向け:

Yコンビネータは、関数に名前を付けることはできないが、引数として渡したり、戻り値として使用したり、他の関数内で定義したりできる状況で、再帰を実装できる式です。

関数を引数として自分自身に渡すことで機能するため、自分自身を呼び出すことができます。

これはラムダ計算の一部です。ラムダ計算は、実際には数学ですが、事実上プログラミング言語であり、コンピューターサイエンス、特に関数型プログラミングの基本です。

プログラミング言語では関数に名前を付ける傾向があるため、Yコンビネーターの日常的な実用的な価値は限られています。

警察のラインナップで識別する必要がある場合は、次のようになります。

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

繰り返されるため、通常はそれを見つけることができ(λx.f (x x))ます。

λシンボルは、その名の結石ラムダを与えるギリシャ文字のラムダ、であり、多くのがあります(λx.t)それが何のようなラムダ計算のルックスだからスタイルの用語が。


これは受け入れられる答えになるはずです。ところで、とU x = x xY = U . (. U)(表記ハスケル状乱用)。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 fx固定小数点方程式の解を表します

               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

where v変数の範囲と、ベータおよびイータ削減ルール

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

ベータリダクションxは、抽象(「関数」)本体内の変数のすべての自由発生をB式(「引数」)で置き換えますE。Etaの削減により、冗長な抽象化が排除されます。形式から省略されることもあります。既約全く削減ルールが適用されない先の式は、である通常のまたは標準的な形式

λ x y. E

の略記です

λ x. λ y. E

(抽象多分岐)、

E F G

の略記です

(E F) G

(アプリケーション左結合性)、

λ x. x

そして

λ y. y

あるアルファ同等

抽象化とアプリケーションは、ラムダ計算の2つの唯一の「言語プリミティブ」ですが、それらは任意に複雑なデータと操作のエンコードを可能にします。

教会の数字は、Peano-axiomatic naturalsに似た自然数のエンコーディングです。

   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

左端の部分式(「ヘッド」)の削減が優先されます。適用順は、置換前に引数を正規化しますが、通常の順序はしません。2つの戦略は、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)

すべての部分式が評価される前に、Haskellの遅延は有限に正規化されます。

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答えは、Yコンビネーターの背後にあるCS /数学の概念についての追加のウォンカイアンストーリーを伝えることを目的としています。おそらく、おなじみの概念に対する可能な限りの類似点は、すでに他の回答者によって描かれていると思います。個人的に、私は常に、本当の起源、アイデアの根本的な目新しさ、素晴らしいアナロジーに直面することを好みました。私は、最も広い類推は不適切で混乱していると思います。

1
こんにちは、IDコンビλ x . xネーターです。今日はどうですか?
MaiaVictor 2017

私はこの答えが一番好きです。それは私の質問をすべてクリアしました!
学生

11

他の回答は、これに対するかなり簡潔な回答を提供しますが、1つの重要な事実はありません。固定小数点コンビネーターをこの複雑な方法で実用的な言語で実装する必要はなく、それを行うことは実用的な目的を果たしません( "見た目、Yコンビネーターはです」)。これは重要な理論的概念ですが、実用的な価値はほとんどありません。


6

Y-CombinatorとFactorial関数のJavaScript実装を以下に示します(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コンビネータは、フラックスコンデンサの別名です。


4
非常に面白い。:)若い(それ以上の)ものは参照を認識しないかもしれません。
Will Ness、

2
はは!はい、若い方(私)はまだ理解できます...

それは本物だと思って、結局ここに来ました。youtube.com/watch?v=HyWqxkaQpPw最近の記事futurism.com/scientists-made-real-life-flux-capacitor
Saw Thinkar Nay Htoo

この答えは、英語を話さない人にとっては特に混乱するかもしれません。それがユーモラスな大衆文化の参照であることを認識する前に(またはまったく)この主張を理解することにかなりの時間を割くかもしれません。(私はそれが好きです、私がこれに答えて、学習している誰かがそれによって落胆したことを発見したならば、私はただ気分が悪くなります)
マイク

5

私はClojureとSchemeの両方でYコンビネーターに一種の「バカガイド」を書いて、自分がそれに慣れるのを手助けしました。彼らは「リトル・スキーマー」の素材に影響されています

スキーム:https : //gist.github.com/z5h/238891

またはClojure:https : //gist.github.com/z5h/5102747

どちらのチュートリアルもコメントが散在しているコードであり、お気に入りのエディターにカットアンドペーストできる必要があります。


5

コンビネーターの初心者として、Mike Vanierの記事を見つけました(Nicholas Mancusoに感謝)が本当に役立つと思いました。私の理解を文書化することに加えて、私は要約を書きたいと思います、それが他の人に役立つかもしれないならば、私はとても嬉しいでしょう。

安っぽい少ない安っぽいです

例として階乗を使用して、次のalmost-factorial関数を使用して数値の階乗を計算しますx

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

上記の疑似コードでalmost-factorialは、関数fと数値を受け取りますxalmost-factorialカリー化されているため、関数を受け取りf、1 値関数を返すと見なすことができます)。

almost-factorial階乗をx計算する場合、階乗の計算x - 1を機能するように委任し、fその結果を累積しますx(この場合、(x-1)の結果にxを乗算します)。

見ることができるalmost-factorial取り込む安っぽい(わずか数まで計算することができる階乗関数のバージョンx - 1)と返しあまり安っぽい(数まで計算階乗のバージョンx)。このフォームのように:

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

階乗の少ないバージョンの階乗を繰り返し渡すと、almost-factorial最終的に目的の階乗関数が得られますf。それは次のように考えることができます:

almost-factorial f = f

固定小数点

それがalmost-factorial f = f意味するのfは、関数の不動点ですalmost-factorial

これは、上記の関数の関係を見るのに非常に興味深い方法であり、私にとってはああの瞬間でした。(もしまだならフィックスポイントのマイクの投稿を読んでください)

3つの機能

一般化するために、我々は非再帰的機能fn(私たちのほとんど-階乗など)を、私たちはその持っている固定小数点機能fr(当社Fなど)、そしてどのようなYあなたが与える時に行うことはあるY fnYの修正点関数を返しますfn

つまり、要約すると(frパラメータを1つだけ取ると仮定して簡略化され、再帰x的にx - 1x - 2...に縮退します):

  • 私たちは、定義するコアの計算を通りfndef fn fr x = ...accumulate x with result from (fr (- x 1))、これはほとんど、便利な機能-私たちが使用することはできませんが、fn上で直接x、それは非常にすぐに有用であろう。この非再帰fnは、関数frを使用して結果を計算します
  • fn fr = frfrの修正点でfnfrある便利な funciton、我々は使用することができるfrx、当社の結果を得るために
  • Y fn = frY関数の固定小数点を返し、ほぼ有用な関数有用もの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)簡素化するために、任意のアリティの関数とすることができる、のは想定してみましょう(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、実際のビジネス・ロジックを定義します。基づいてfnY作成するfr-特定の形式のヘルパー機能は-の計算を容易にするためfn、再帰的方法。

Y現時点でこの方法を理解するのに役立ちました。

ところで、私は「ラムダ計算による関数型プログラミング入門という本も非常に優れていることを知りました。私はそれを一部に過ぎず、この本に頭を悩ませることができなかったという事実Yが私をこの投稿に導きました。


5

Nicholas Mancuso回答で言及された記事(これは読む価値があります)から編集され た元の質問に対する回答と、その他の回答です。

Yコンビネーターとは何ですか?

Y-combinatorは「関数型」(またはより高次の関数—他の関数を操作する関数)であり、単一の引数を取ります。これは再帰的ではない関数であり、次の関数のバージョンを返します再帰的。


やや再帰的な=)ですが、より詳細な定義:

コンビネータ—自由変数のない単なるラムダ式です。
自由変数—バインドされた変数ではない変数です。
バインドされた変数—引数の1つとしてその変数名を持つラムダ式の本体内に含まれる変数。

これについて考えるもう1つの方法は、combinatorがラムダ式であり、combinatorの名前をその定義がある場所でその定義に置き換えることができ、すべてがまだ機能することです(combinatorが無限ループに入る場合)ラムダ本体内のそれ自体への参照を含みます)。

Y-combinatorは固定小数点コンビネーターです。

関数の固定小数点は、関数によってそれ自体にマップされる関数のドメインの要素です。
つまり、ある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)))

それはその性質において怠惰なものと同じです、それはλラムダのボディ評価を遅らせる追加のラッパーを持っているだけです。このトピックに多少関係のある別の質問をしました。

彼らは何のために良いのですか?

盗まれたがから借りクリスAmmermanによって解答問題の関数の実際の作業からそれを分離することにより、その実装を抽象化し、そして、Yコンビネータの一般化再帰:。

Yコンビネーターにはいくつかの実用的なアプリケーションがありますが、それは主に理論的な概念であり、それを理解することで全体的なビジョンが広がり、おそらく分析スキルと開発者スキルが向上します。

それらは手続き型言語で役立ちますか?

マイク・バニエで述べた「多くの静的型付け言語でYコンビネータを定義することは可能であるが、(少なくとも、私が見てきた例で)Yコンビネータ自体はdoesnのため、このような定義は通常、いくつかの非自明なタイプのハックが必要ですt単純な静的型があります。それはこの記事の範囲を超えているので、これ以上は触れません

そして、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は分岐/無限ループします)。


Yコンビネーターは、値渡しと遅延評価で機能します。
Quelklef

3

固定小数点コンビネーター(または固定小数点演算子)は、他の関数の固定小数点を計算する高次関数です。この操作は、言語のランタイムエンジンからの明示的なサポートなしに、書き換えルールの形で再帰を実装できるため、プログラミング言語理論に関連しています。(srcウィキペディア)


3

this-operatorはあなたの人生を簡素化することができます:

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スカイタワー、2016年

1
ノクティス、質問を明確にしてもらえますか?y-combinatorの概念自体が解決するよりも多くの問題を作成するかどうかを尋ねていますか、それとも私がJavaScriptを使用してデモンストレーションすることを選択したことを具体的に話しているのですか?説明した?
zumalifeguard 2016年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.